Replaced Boost.Log with small custom logger

Boost.Log is a complex monstrosity and I can't get it to build on OS X.
This commit is contained in:
Daniel Wolf 2016-04-13 22:37:39 +02:00
parent 4941bff739
commit 7ce79f9c08
10 changed files with 261 additions and 164 deletions

View File

@ -36,10 +36,6 @@ elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
set(enableWarningsFlags "/W4") set(enableWarningsFlags "/W4")
set(disableWarningsFlags "/W0") set(disableWarningsFlags "/W0")
# Disable warning C4714: function '...' marked as __forceinline not inlined
# This is caused by Boost.Log
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4714")
# Disable warning C4456: declaration of '...' hides previous local declaration # Disable warning C4456: declaration of '...' hides previous local declaration
# I'm doing that on purpose. # I'm doing that on purpose.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4458") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4458")
@ -54,7 +50,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(Boost_USE_STATIC_LIBS ON) # Use static libs set(Boost_USE_STATIC_LIBS ON) # Use static libs
set(Boost_USE_MULTITHREADED ON) # Enable multithreading support set(Boost_USE_MULTITHREADED ON) # Enable multithreading support
set(Boost_USE_STATIC_RUNTIME ON) # Use static C++ runtime set(Boost_USE_STATIC_RUNTIME ON) # Use static C++ runtime
find_package(Boost REQUIRED COMPONENTS filesystem locale system log date_time thread chrono) find_package(Boost REQUIRED COMPONENTS filesystem locale system)
include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) include_directories(SYSTEM ${Boost_INCLUDE_DIRS})
# ... C++ Format # ... C++ Format

View File

@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp> #include <boost/property_tree/xml_parser.hpp>
#include <tools.h>
using std::string; using std::string;
using boost::property_tree::ptree; using boost::property_tree::ptree;

View File

@ -48,7 +48,7 @@ Timeline<bool> detectVoiceActivity(std::unique_ptr<AudioStream> audioStream) {
// Log // Log
for (const auto& element : activity) { for (const auto& element : activity) {
logTimedEvent("utterance", static_cast<TimeRange>(element), std::string()); logging::logTimedEvent("utterance", static_cast<TimeRange>(element), std::string());
} }
return activity; return activity;

View File

@ -1,123 +1,131 @@
#include "logging.h" #include "logging.h"
#include <boost/log/sinks/unlocked_frontend.hpp> #include <tools.h>
#include <boost/log/sinks/text_file_backend.hpp> #include <iostream>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/keywords/file_name.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
// ReSharper disable once CppUnusedIncludeDirective
#include <boost/log/support/date_time.hpp>
#include <Timed.h>
using namespace logging;
using std::string; using std::string;
using std::lock_guard;
using boost::log::sinks::text_ostream_backend;
using boost::log::record_view;
using boost::log::sinks::unlocked_sink;
using std::vector; using std::vector;
using std::tuple; using std::tuple;
using std::make_tuple; using std::make_tuple;
using std::shared_ptr;
namespace expr = boost::log::expressions; using std::lock_guard;
namespace keywords = boost::log::keywords;
namespace sinks = boost::log::sinks;
namespace attr = boost::log::attributes;
template <> template <>
const string& getEnumTypeName<LogLevel>() { const string& getEnumTypeName<Level>() {
static const string name = "LogLevel"; static const string name = "LogLevel";
return name; return name;
} }
template <> template <>
const vector<tuple<LogLevel, string>>& getEnumMembers<LogLevel>() { const vector<tuple<Level, string>>& getEnumMembers<Level>() {
static const vector<tuple<LogLevel, string>> values = { static const vector<tuple<Level, string>> values = {
make_tuple(LogLevel::Trace, "Trace"), make_tuple(Level::Trace, "Trace"),
make_tuple(LogLevel::Debug, "Debug"), make_tuple(Level::Debug, "Debug"),
make_tuple(LogLevel::Info, "Info"), make_tuple(Level::Info, "Info"),
make_tuple(LogLevel::Warning, "Warning"), make_tuple(Level::Warn, "Warn"),
make_tuple(LogLevel::Error, "Error"), make_tuple(Level::Error, "Error"),
make_tuple(LogLevel::Fatal, "Fatal") make_tuple(Level::Fatal, "Fatal")
}; };
return values; return values;
} }
std::ostream& operator<<(std::ostream& stream, LogLevel value) { std::ostream& operator<<(std::ostream& stream, Level value) {
return stream << enumToString(value); return stream << enumToString(value);
} }
std::istream& operator>>(std::istream& stream, LogLevel& value) { std::istream& operator>>(std::istream& stream, Level& value) {
string name; string name;
stream >> name; stream >> name;
value = parseEnum<LogLevel>(name); value = parseEnum<Level>(name);
return stream; return stream;
} }
PausableBackendAdapter::PausableBackendAdapter(boost::shared_ptr<text_ostream_backend> backend) : Entry::Entry(Level level, const string& message) :
backend(backend) {} level(level),
message(message)
PausableBackendAdapter::~PausableBackendAdapter() { {
resume(); time(&timestamp);
} }
void PausableBackendAdapter::consume(const record_view& recordView, const string message) { string SimpleConsoleFormatter::format(const Entry& entry) {
lock_guard<std::mutex> lock(mutex); return fmt::format("[{0}] {1}", entry.level, entry.message);
if (isPaused) { }
buffer.push_back(std::make_tuple(recordView, message));
} else { string SimpleFileFormatter::format(const Entry& entry) {
backend->consume(recordView, message); return fmt::format("[{0}] {1}", formatTime(entry.timestamp, "%F %H:%M:%S"), consoleFormatter.format(entry));
}
LevelFilter::LevelFilter(shared_ptr<Sink> innerSink, Level minLevel) :
innerSink(innerSink),
minLevel(minLevel)
{}
void LevelFilter::receive(const Entry& entry) {
if (entry.level >= minLevel) {
innerSink->receive(entry);
} }
} }
void PausableBackendAdapter::pause() { StreamSink::StreamSink(shared_ptr<std::ostream> stream, shared_ptr<Formatter> formatter) :
lock_guard<std::mutex> lock(mutex); stream(stream),
isPaused = true; formatter(formatter)
{}
void StreamSink::receive(const Entry& entry) {
string line = formatter->format(entry);
*stream << line << std::endl;
} }
void PausableBackendAdapter::resume() { StdErrSink::StdErrSink(shared_ptr<Formatter> formatter) :
StreamSink(std::shared_ptr<std::ostream>(&std::cerr, [](void*){}), formatter)
{}
PausableSink::PausableSink(shared_ptr<Sink> innerSink) :
innerSink(innerSink)
{}
void PausableSink::receive(const Entry& entry) {
lock_guard<std::mutex> lock(mutex);
if (isPaused) {
buffer.push_back(entry);
} else {
innerSink->receive(entry);
}
}
void PausableSink::pause() {
lock_guard<std::mutex> lock(mutex);
isPaused = true;
}
void PausableSink::resume() {
lock_guard<std::mutex> lock(mutex); lock_guard<std::mutex> lock(mutex);
isPaused = false; isPaused = false;
for (const auto& tuple : buffer) { for (const Entry& entry : buffer) {
backend->consume(std::get<record_view>(tuple), std::get<string>(tuple)); innerSink->receive(entry);
} }
buffer.clear(); buffer.clear();
} }
BOOST_LOG_GLOBAL_LOGGER_INIT(globalLogger, LoggerType) { std::mutex& getLogMutex() {
LoggerType logger; static std::mutex mutex;
return mutex;
logger.add_attribute("TimeStamp", attr::local_clock());
return logger;
} }
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", LogLevel) vector<shared_ptr<Sink>>& getSinks() {
static vector<shared_ptr<Sink>> sinks;
boost::shared_ptr<PausableBackendAdapter> addPausableStderrSink(LogLevel minLogLevel) { return sinks;
// Create logging backend that logs to stderr
auto streamBackend = boost::make_shared<text_ostream_backend>();
streamBackend->add_stream(boost::shared_ptr<std::ostream>(&std::cerr, [](std::ostream*) {}));
streamBackend->auto_flush(true);
// Create an adapter that allows us to pause, buffer, and resume log output
auto pausableAdapter = boost::make_shared<PausableBackendAdapter>(streamBackend);
// Create a sink that feeds into the adapter
auto sink = boost::make_shared<unlocked_sink<PausableBackendAdapter>>(pausableAdapter);
sink->set_formatter(expr::stream << "[" << severity << "] " << expr::smessage);
sink->set_filter(severity >= minLogLevel);
boost::log::core::get()->add_sink(sink);
return pausableAdapter;
} }
void addFileSink(const boost::filesystem::path& logFilePath, LogLevel minLogLevel) { void logging::addSink(shared_ptr<Sink> sink) {
auto textFileBackend = boost::make_shared<sinks::text_file_backend>( lock_guard<std::mutex> lock(getLogMutex());
keywords::file_name = logFilePath.string()); getSinks().push_back(sink);
auto sink = boost::make_shared<sinks::synchronous_sink<sinks::text_file_backend>>(textFileBackend); }
sink->set_formatter(expr::stream
<< "[" << expr::format_date_time<boost::posix_time::ptime>("TimeStamp", "%Y-%m-%d %H:%M:%S") void logging::log(Level level, const string& message) {
<< "] [" << severity << "] " << expr::smessage); lock_guard<std::mutex> lock(getLogMutex());
sink->set_filter(severity >= minLogLevel); for (auto& sink : getSinks()) {
boost::log::core::get()->add_sink(sink); sink->receive(Entry(level, message));
}
} }

View File

@ -1,86 +1,145 @@
#pragma once #pragma once
#include <boost/log/common.hpp>
#include <boost/log/sinks/basic_sink_backend.hpp>
#include <boost/log/sinks/frontend_requirements.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <string> #include <string>
#include <vector> #include <vector>
#include <mutex> #include <mutex>
#include <tuple> #include <tuple>
#include "centiseconds.h"
#include <boost/filesystem.hpp>
#include "tools.h"
#include "enumTools.h" #include "enumTools.h"
#include "tools.h"
#include "Timed.h" #include "Timed.h"
enum class LogLevel { namespace logging {
enum class Level {
Trace, Trace,
Debug, Debug,
Info, Info,
Warning, Warn,
Error, Error,
Fatal, Fatal,
EndSentinel EndSentinel
}; };
}
template<> template<>
const std::string& getEnumTypeName<LogLevel>(); const std::string& getEnumTypeName<logging::Level>();
template<> template<>
const std::vector<std::tuple<LogLevel, std::string>>& getEnumMembers<LogLevel>(); const std::vector<std::tuple<logging::Level, std::string>>& getEnumMembers<logging::Level>();
std::ostream& operator<<(std::ostream& stream, LogLevel value); std::ostream& operator<<(std::ostream& stream, logging::Level value);
std::istream& operator>>(std::istream& stream, LogLevel& value); std::istream& operator>>(std::istream& stream, logging::Level& value);
using LoggerType = boost::log::sources::severity_logger_mt<LogLevel>; namespace logging {
BOOST_LOG_GLOBAL_LOGGER(globalLogger, LoggerType) struct Entry {
Entry(Level level, const std::string& message);
#define LOG(level) \ time_t timestamp;
BOOST_LOG_STREAM_WITH_PARAMS(globalLogger::get(), (::boost::log::keywords::severity = level)) Level level;
std::string message;
};
#define LOG_TRACE LOG(LogLevel::Trace) class Formatter {
#define LOG_DEBUG LOG(LogLevel::Debug) public:
#define LOG_INFO LOG(LogLevel::Info) virtual ~Formatter() = default;
#define LOG_WARNING LOG(LogLevel::Warning) virtual std::string format(const Entry& entry) = 0;
#define LOG_ERROR LOG(LogLevel::Error) };
#define LOG_FATAL LOG(LogLevel::Fatal)
class PausableBackendAdapter : class SimpleConsoleFormatter : public Formatter {
public boost::log::sinks::basic_formatted_sink_backend<char, boost::log::sinks::concurrent_feeding> public:
{ std::string format(const Entry& entry) override;
public: };
PausableBackendAdapter(boost::shared_ptr<boost::log::sinks::text_ostream_backend> backend);
~PausableBackendAdapter(); class SimpleFileFormatter : public Formatter {
void consume(const boost::log::record_view& recordView, const std::string message); public:
std::string format(const Entry& entry) override;
private:
SimpleConsoleFormatter consoleFormatter;
};
class Sink {
public:
virtual ~Sink() = default;
virtual void receive(const Entry& entry) = 0;
};
class LevelFilter : public Sink {
public:
LevelFilter(std::shared_ptr<Sink> innerSink, Level minLevel);
void receive(const Entry& entry) override;
private:
std::shared_ptr<Sink> innerSink;
Level minLevel;
};
class StreamSink : public Sink {
public:
StreamSink(std::shared_ptr<std::ostream> stream, std::shared_ptr<Formatter> formatter);
void receive(const Entry& entry) override;
private:
std::shared_ptr<std::ostream> stream;
std::shared_ptr<Formatter> formatter;
};
class StdErrSink : public StreamSink {
public:
explicit StdErrSink(std::shared_ptr<Formatter> formatter);
};
class PausableSink : public Sink {
public:
explicit PausableSink(std::shared_ptr<Sink> innerSink);
void receive(const Entry& entry) override;
void pause(); void pause();
void resume(); void resume();
private: private:
boost::shared_ptr<boost::log::sinks::text_ostream_backend> backend; std::shared_ptr<Sink> innerSink;
std::vector<std::tuple<boost::log::record_view, std::string>> buffer; std::vector<Entry> buffer;
std::mutex mutex; std::mutex mutex;
bool isPaused = false; bool isPaused = false;
}; };
boost::shared_ptr<PausableBackendAdapter> addPausableStderrSink(LogLevel minLogLevel); void addSink(std::shared_ptr<Sink> sink);
void addFileSink(const boost::filesystem::path& logFilePath, LogLevel minLogLevel); void log(Level level, const std::string& message);
template<typename TValue> template <typename... Args>
void logTimedEvent(const std::string& eventName, const Timed<TValue> timedValue) { void logFormat(Level level, fmt::CStringRef format, const Args&... args) {
LOG_DEBUG log(level, fmt::format(format, args...));
<< "##" << eventName << "[" << formatDuration(timedValue.getStart()) << "-" << formatDuration(timedValue.getEnd()) << "]: " }
<< timedValue.getValue();
}
template<typename TValue> #define LOG_WITH_LEVEL(levelName, levelEnum) \
void logTimedEvent(const std::string& eventName, const TimeRange& timeRange, const TValue& value) { inline void levelName(const std::string& message) { \
log(Level::levelEnum, message); \
} \
template <typename... Args> \
void levelName ## Format(fmt::CStringRef format, const Args&... args) { \
logFormat(Level::levelEnum, format, args...); \
}
LOG_WITH_LEVEL(trace, Trace)
LOG_WITH_LEVEL(debug, Debug)
LOG_WITH_LEVEL(info, Info)
LOG_WITH_LEVEL(warn, Warn)
LOG_WITH_LEVEL(error, Error)
LOG_WITH_LEVEL(fatal, Fatal)
template<typename TValue>
void logTimedEvent(const std::string& eventName, const Timed<TValue> timedValue) {
debugFormat("##{0} [{1}-{2}]: {3}",
eventName, formatDuration(timedValue.getStart()), formatDuration(timedValue.getEnd()), timedValue.getValue());
}
template<typename TValue>
void logTimedEvent(const std::string& eventName, const TimeRange& timeRange, const TValue& value) {
logTimedEvent(eventName, Timed<TValue>(timeRange, value)); logTimedEvent(eventName, Timed<TValue>(timeRange, value));
} }
template<typename TValue> template<typename TValue>
void logTimedEvent(const std::string& eventName, centiseconds start, centiseconds end, const TValue& value) { void logTimedEvent(const std::string& eventName, centiseconds start, centiseconds end, const TValue& value) {
logTimedEvent(eventName, Timed<TValue>(start, end, value)); logTimedEvent(eventName, Timed<TValue>(start, end, value));
}
} }

View File

@ -18,6 +18,8 @@ using std::string;
using std::vector; using std::vector;
using std::unique_ptr; using std::unique_ptr;
using std::make_unique; using std::make_unique;
using std::shared_ptr;
using std::make_shared;
using std::map; using std::map;
using std::chrono::duration; using std::chrono::duration;
using std::chrono::duration_cast; using std::chrono::duration_cast;
@ -47,7 +49,7 @@ unique_ptr<AudioStream> createAudioStream(path filePath) {
// Tell TCLAP how to handle our types // Tell TCLAP how to handle our types
namespace TCLAP { namespace TCLAP {
template<> template<>
struct ArgTraits<LogLevel> { struct ArgTraits<logging::Level> {
typedef ValueLike ValueCategory; typedef ValueLike ValueCategory;
}; };
template<> template<>
@ -56,8 +58,25 @@ namespace TCLAP {
}; };
} }
shared_ptr<logging::PausableSink> addPausableStdErrSink(logging::Level minLevel) {
auto stdErrSink = make_shared<logging::StdErrSink>(make_shared<logging::SimpleConsoleFormatter>());
auto pausableSink = make_shared<logging::PausableSink>(stdErrSink);
auto levelFilter = make_shared<logging::LevelFilter>(pausableSink, minLevel);
logging::addSink(levelFilter);
return pausableSink;
}
void addFileSink(path path, logging::Level minLevel) {
auto file = make_shared<boost::filesystem::ofstream>();
file->exceptions(std::ifstream::failbit | std::ifstream::badbit);
file->open(path);
auto FileSink = make_shared<logging::StreamSink>(file, make_shared<logging::SimpleFileFormatter>());
auto levelFilter = make_shared<logging::LevelFilter>(FileSink, minLevel);
logging::addSink(levelFilter);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
auto pausableStderrSink = addPausableStderrSink(LogLevel::Warning); auto pausableStderrSink = addPausableStdErrSink(logging::Level::Warn);
pausableStderrSink->pause(); pausableStderrSink->pause();
// Define command-line parameters // Define command-line parameters
@ -65,9 +84,9 @@ int main(int argc, char *argv[]) {
tclap::CmdLine cmd(appName, argumentValueSeparator, appVersion); tclap::CmdLine cmd(appName, argumentValueSeparator, appVersion);
cmd.setExceptionHandling(false); cmd.setExceptionHandling(false);
cmd.setOutput(new NiceCmdLineOutput()); cmd.setOutput(new NiceCmdLineOutput());
auto logLevels = vector<LogLevel>(getEnumValues<LogLevel>()); auto logLevels = vector<logging::Level>(getEnumValues<logging::Level>());
tclap::ValuesConstraint<LogLevel> logLevelConstraint(logLevels); tclap::ValuesConstraint<logging::Level> logLevelConstraint(logLevels);
tclap::ValueArg<LogLevel> logLevel("", "logLevel", "The minimum log level to log", false, LogLevel::Debug, &logLevelConstraint, cmd); tclap::ValueArg<logging::Level> logLevel("", "logLevel", "The minimum log level to log", false, logging::Level::Debug, &logLevelConstraint, cmd);
tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd); tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd);
tclap::ValueArg<string> dialog("d", "dialog", "The text of the dialog.", false, string(), "string", cmd); tclap::ValueArg<string> dialog("d", "dialog", "The text of the dialog.", false, string(), "string", cmd);
auto exportFormats = vector<ExportFormat>(getEnumValues<ExportFormat>()); auto exportFormats = vector<ExportFormat>(getEnumValues<ExportFormat>());

View File

@ -72,7 +72,7 @@ Timeline<Shape> animate(const Timeline<Phone> &phones) {
for (auto& timedPhone : phones) { for (auto& timedPhone : phones) {
Timed<Shape> timedShape(static_cast<TimeRange>(timedPhone), getShape(timedPhone.getValue())); Timed<Shape> timedShape(static_cast<TimeRange>(timedPhone), getShape(timedPhone.getValue()));
shapes.set(timedShape); shapes.set(timedShape);
logTimedEvent("shape", timedShape); logging::logTimedEvent("shape", timedShape);
} }
return shapes; return shapes;

View File

@ -98,18 +98,18 @@ void processAudioStream(AudioStream& audioStream16kHz, function<void(const vecto
} while (buffer.size()); } while (buffer.size());
} }
LogLevel ConvertSphinxErrorLevel(err_lvl_t errorLevel) { logging::Level ConvertSphinxErrorLevel(err_lvl_t errorLevel) {
switch (errorLevel) { switch (errorLevel) {
case ERR_DEBUG: case ERR_DEBUG:
case ERR_INFO: case ERR_INFO:
case ERR_INFOCONT: case ERR_INFOCONT:
return LogLevel::Trace; return logging::Level::Trace;
case ERR_WARN: case ERR_WARN:
return LogLevel::Warning; return logging::Level::Warn;
case ERR_ERROR: case ERR_ERROR:
return LogLevel::Error; return logging::Level::Error;
case ERR_FATAL: case ERR_FATAL:
return LogLevel::Fatal; return logging::Level::Fatal;
default: default:
throw invalid_argument("Unknown log level."); throw invalid_argument("Unknown log level.");
} }
@ -137,8 +137,8 @@ void sphinxLogCallback(void* user_data, err_lvl_t errorLevel, const char* format
string message(chars.data()); string message(chars.data());
boost::algorithm::trim(message); boost::algorithm::trim(message);
LogLevel logLevel = ConvertSphinxErrorLevel(errorLevel); logging::Level logLevel = ConvertSphinxErrorLevel(errorLevel);
LOG(logLevel) << message; logging::log(logLevel, message);
} }
vector<string> recognizeWords(unique_ptr<AudioStream> audioStream, ps_decoder_t& recognizer, ProgressSink& progressSink) { vector<string> recognizeWords(unique_ptr<AudioStream> audioStream, ps_decoder_t& recognizer, ProgressSink& progressSink) {
@ -169,7 +169,7 @@ vector<string> recognizeWords(unique_ptr<AudioStream> audioStream, ps_decoder_t&
int firstFrame, lastFrame; int firstFrame, lastFrame;
ps_seg_frames(it, &firstFrame, &lastFrame); ps_seg_frames(it, &firstFrame, &lastFrame);
logTimedEvent("word", centiseconds(firstFrame), centiseconds(lastFrame + 1), word); logging::logTimedEvent("word", centiseconds(firstFrame), centiseconds(lastFrame + 1), word);
} }
return result; return result;
@ -278,7 +278,7 @@ Timeline<Phone> getPhoneAlignment(const vector<s3wid_t>& wordIds, unique_ptr<Aud
Timed<Phone> timedPhone(start, start + duration, parseEnum<Phone>(phoneName)); Timed<Phone> timedPhone(start, start + duration, parseEnum<Phone>(phoneName));
result.set(timedPhone); result.set(timedPhone);
logTimedEvent("phone", timedPhone); logging::logTimedEvent("phone", timedPhone);
} }
return result; return result;
} }

View File

@ -1,6 +1,7 @@
#include "tools.h" #include "tools.h"
#include <format.h> #include <format.h>
#include <chrono> #include <chrono>
#include <vector>
using std::string; using std::string;
using std::chrono::duration; using std::chrono::duration;
@ -8,3 +9,14 @@ using std::chrono::duration;
string formatDuration(duration<double> seconds) { string formatDuration(duration<double> seconds) {
return fmt::format("{0:.2f}", seconds.count()); return fmt::format("{0:.2f}", seconds.count());
} }
string formatTime(time_t time, const string& format) {
tm* timeInfo = localtime(&time);
std::vector<char> buffer(20);
bool success = false;
while (!success) {
success = strftime(buffer.data(), buffer.size(), format.c_str(), timeInfo) != 0;
if (!success) buffer.resize(buffer.size() * 2);
}
return string(buffer.data());
}

View File

@ -10,3 +10,5 @@ template<typename T>
using lambda_unique_ptr = std::unique_ptr<T, std::function<void(T*)>>; using lambda_unique_ptr = std::unique_ptr<T, std::function<void(T*)>>;
std::string formatDuration(std::chrono::duration<double> seconds); std::string formatDuration(std::chrono::duration<double> seconds);
std::string formatTime(time_t time, const std::string& format);