From c71913ded9e417282db7254635522822a35383ca Mon Sep 17 00:00:00 2001 From: "Christopher M. Punches" Date: Tue, 18 Aug 2026 02:57:47 -0400 Subject: [PATCH] Configuration and logging move out of the context source context.cpp had grown to hold three unrelated concerns: the context lifecycle, everything about the .conf file format, and everything about writing a log message. conf.cpp now owns the format end to end - parsing a file into the store, reading values back out, and interpreting a configured string as the boolean or log level a setting holds. logging.cpp owns the write path: the level filter, the stream choice, and the name a level carries in output. context.cpp keeps what a context is: resolving each setting from the override, the configured value, or the built-in default, and reporting what it resolved. --- CMakeLists.txt | 2 + include/internal/conf.hpp | 61 ++++++++ include/internal/context.hpp | 10 -- src/cli/dpm.cpp | 4 +- src/core/conf.cpp | 244 ++++++++++++++++++++++++++++++ src/core/context.cpp | 279 +---------------------------------- src/core/logging.cpp | 112 ++++++++++++++ 7 files changed, 429 insertions(+), 283 deletions(-) create mode 100644 include/internal/conf.hpp create mode 100644 src/core/conf.cpp create mode 100644 src/core/logging.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 14ecdb8..1e54e1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # libdpm-core.so — the library (C ABI) # --------------------------------------------------------------------- add_library(dpm-core SHARED + src/core/conf.cpp src/core/context.cpp + src/core/logging.cpp src/core/modules.cpp src/core/version.cpp ) diff --git a/include/internal/conf.hpp b/include/internal/conf.hpp new file mode 100644 index 0000000..7a145a8 --- /dev/null +++ b/include/internal/conf.hpp @@ -0,0 +1,61 @@ +/** + * @file conf.hpp + * @brief Reading the .conf files, and interpreting the values they carry + * + * @copyright Copyright (c) 2026 SILO GROUP LLC + * @author Chris Punches + * + * Part of the Dark Horse Linux Package Manager (DPM) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +struct dpm_ctx; + +namespace dpm_core { + /** + * @brief Reads every .conf file in a context's configuration directory + * + * Each file becomes one namespace in the store, named after the file + * without its extension, so core.conf fills "core" and mymodule.conf + * fills "mymodule". Files with any other extension are ignored. + * + * A missing or unreadable directory leaves the store empty, which + * leaves every setting at its default. + * + * @param ctx The libdpm-core.so context + */ + void load_config_dir(dpm_ctx* ctx); + + /** + * @brief Interprets a configured boolean value + * + * @param v The configured value + * @param fallback Returned when the value matches no known spelling + * @return The interpreted boolean, or the fallback + */ + bool parse_bool(const std::string& v, bool fallback); + + /** + * @brief Maps a configured log level name to its numeric level + * + * @param v The level name read from configuration + * @param fallback Returned when the name is unrecognized + * @return A DPM_LOG_* value, or the fallback + */ + int log_level_str2enum(const std::string& v, int fallback); +} // namespace dpm_core diff --git a/include/internal/context.hpp b/include/internal/context.hpp index 082fa24..6d3c1ff 100644 --- a/include/internal/context.hpp +++ b/include/internal/context.hpp @@ -90,14 +90,4 @@ namespace dpm_core { * @param msg The failure description */ void set_error(dpm_ctx* ctx, const std::string& msg); - - /** - * @brief Loads all configuration files into the context - * - * Parses every .conf file in the context's configuration directory - * into the context's configuration store. - * - * @param ctx The libdpm-core.so context - */ - void load_config_dir(dpm_ctx* ctx); } // namespace dpm_core diff --git a/src/cli/dpm.cpp b/src/cli/dpm.cpp index 4d96e12..e05644b 100644 --- a/src/cli/dpm.cpp +++ b/src/cli/dpm.cpp @@ -64,7 +64,7 @@ namespace { * @param name The level name (case-insensitive) * @return The DPM_LOG_* level, or -1 if unrecognized */ - int level_from_name(const char* name) { + int log_level_str2enum(const char* name) { if (strcasecmp(name, "FATAL") == 0) { return DPM_LOG_FATAL; } if (strcasecmp(name, "ERROR") == 0) { return DPM_LOG_ERROR; } if (strcasecmp(name, "WARN") == 0) { return DPM_LOG_WARN; } @@ -202,7 +202,7 @@ int main(int argc, char** argv) { } else if (option_matches(arg, "-L", "--log-level", &inline_value)) { const char* v = take_value(); if (!v) { return 1; } - int level = level_from_name(v); + int level = log_level_str2enum(v); if (level < 0) { std::fprintf(stderr, "dpm: unknown log level '%s' (use FATAL, ERROR, " diff --git a/src/core/conf.cpp b/src/core/conf.cpp new file mode 100644 index 0000000..3c42f82 --- /dev/null +++ b/src/core/conf.cpp @@ -0,0 +1,244 @@ +/** + * @file conf.cpp + * @brief The .conf file format: reading it, storing it, interpreting it + * + * Owns everything about configuration files. The format is sectioned + * key/value text: + * + * @code + * [logging] + * log_level = INFO + * write_to_log = false + * @endcode + * + * A line is trimmed, then classified. Empty lines and lines opening with + * '#' or ';' are comments. A line wrapped in square brackets opens a + * section. Anything else is split at the first '=' into a key and a + * value, each trimmed. A line fitting none of those shapes is skipped, + * so a malformed line costs that line and no more. + * + * Values land in the context's store under three keys: the namespace, + * which is the file's name without its extension; the section; and the + * key. Keys appearing before any section header file under "main". + * load_config_dir reads every .conf in the configuration directory, so + * core.conf fills the "core" namespace and mymodule.conf fills + * "mymodule". dpm_config_get reads back out of it. + * + * Everything in the store is text, so reading a setting out of it ends + * in a conversion. The converters live here beside the parser: each + * takes the configured spelling and a fallback, and a value it cannot + * interpret yields that fallback rather than a guess. + * + * @copyright Copyright (c) 2026 SILO GROUP LLC + * @author Chris Punches + * + * Part of the Dark Horse Linux Package Manager (DPM) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "internal/conf.hpp" + +#include "internal/context.hpp" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +/** + * @brief Helpers private to this translation unit + * + * The line-level work behind parsing. An unnamed namespace gives them + * internal linkage, so they are unreachable from the library's other + * files and cannot collide with a same-named helper in one of them. + */ +namespace { + /** + * @brief Strips leading and trailing whitespace + * + * Applied to every line, section name, key, and value, so that + * surrounding spaces and the line's terminator never reach the store. + * + * @param s The string to trim + * @return The trimmed string; empty when the input is entirely whitespace + */ + std::string trim(const std::string& s) { + const char* ws = " \t\r\n\f\v"; + + size_t start = s.find_first_not_of(ws); + + // Every character is whitespace, so nothing survives trimming. + if (start == std::string::npos) { + return ""; + } + + size_t end = s.find_last_not_of(ws); + return s.substr(start, end - start + 1); + } + + /** + * @brief Reads one configuration file into the store + * + * A file that cannot be opened contributes nothing and is not an + * error: configuration is optional, and defaults cover its absence. + * + * @param ctx The context whose store receives the values + * @param file Path of the file to read + * @param module_name The namespace the values are filed under + */ + void parse_config_file(dpm_ctx* ctx, const fs::path& file, + const std::string& module_name) { + std::ifstream in(file); + if (!in.is_open()) { + return; + } + + std::string line; + + // Keys read before the first section header belong to "main". + std::string section = "main"; + + while (std::getline(in, line)) { + line = trim(line); + + // Blank line or comment. + if (line.empty() || line[0] == '#' || line[0] == ';') { + continue; + } + + // Section header: everything after it files under this name. + if (line.front() == '[' && line.back() == ']') { + std::string name = trim(line.substr(1, line.length() - 2)); + section = name.empty() ? "main" : name; + continue; + } + + // Anything without a separator is not a key/value pair. + size_t eq = line.find('='); + if (eq == std::string::npos) { + continue; + } + + std::string key = trim(line.substr(0, eq)); + std::string value = trim(line.substr(eq + 1)); + + // A separator with nothing before it names no key. + if (key.empty()) { + continue; + } + + ctx->config[module_name][section][key] = value; + } + } +} // namespace + +namespace dpm_core { + /** + * @brief Interprets a configured boolean value + * + * Accepts the spellings a configuration file is likely to carry, in + * any case. A value that matches none of them leaves the setting at + * the caller's fallback rather than guessing. + */ + bool parse_bool(const std::string& v, bool fallback) { + // Compare case-insensitively by folding a copy to lowercase. + std::string lower; + for (char c : v) { + lower += static_cast(std::tolower(static_cast(c))); + } + + if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") { + return true; + } + if (lower == "false" || lower == "no" || lower == "0" || lower == "off") { + return false; + } + + return fallback; + } + + /** + * @brief Maps a configured log level name to its numeric level + * + * The comparison is exact and case-sensitive, matching the spelling + * the shipped core.conf uses. + */ + int log_level_str2enum(const std::string& v, int fallback) { + if (v == "FATAL") { return DPM_LOG_FATAL; } + if (v == "ERROR") { return DPM_LOG_ERROR; } + if (v == "WARN") { return DPM_LOG_WARN; } + if (v == "INFO") { return DPM_LOG_INFO; } + if (v == "DEBUG") { return DPM_LOG_DEBUG; } + return fallback; + } + + void load_config_dir(dpm_ctx* ctx) { + std::error_code ec; + if (!fs::is_directory(ctx->config_dir, ec)) { + return; + } + + for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) { + // Stop on a directory read error rather than iterating further. + if (ec) { + break; + } + if (!entry.is_regular_file()) { + continue; + } + if (entry.path().extension() != ".conf") { + continue; + } + + // The namespace is the filename without its extension. + parse_config_file(ctx, entry.path(), entry.path().stem().string()); + } + } +} // namespace dpm_core + +extern "C" { + /** + * @brief Reads a configured value + * + * Values are addressed by the three levels the store is keyed on: the + * namespace a file supplied, a section within it, and a key within + * that section. A level that is absent yields NULL, which the caller + * reads as unset. + */ + const char* dpm_config_get(dpm_ctx* ctx, const char* module, + const char* section, const char* key) { + if (!ctx || !module || !section || !key) { + return nullptr; + } + + auto mod_it = ctx->config.find(module); + if (mod_it == ctx->config.end()) { + return nullptr; + } + + auto sec_it = mod_it->second.find(section); + if (sec_it == mod_it->second.end()) { + return nullptr; + } + + auto key_it = sec_it->second.find(key); + if (key_it == sec_it->second.end()) { + return nullptr; + } + + return key_it->second.c_str(); + } +} /* extern "C" */ diff --git a/src/core/context.cpp b/src/core/context.cpp index 525eb37..eb434ed 100644 --- a/src/core/context.cpp +++ b/src/core/context.cpp @@ -1,6 +1,6 @@ /** * @file context.cpp - * @brief Context lifecycle, configuration, logging, and services + * @brief Context lifecycle, configuration, and services * * @copyright Copyright (c) 2026 SILO GROUP LLC * @author Chris Punches @@ -22,12 +22,9 @@ */ #include "internal/context.hpp" -#include "internal/version.hpp" +#include "internal/conf.hpp" -#include -#include #include -#include #include namespace fs = std::filesystem; @@ -35,10 +32,10 @@ namespace fs = std::filesystem; /** * @brief Helpers private to this translation unit * - * Configuration reading and the value conversions it needs. An unnamed - * namespace gives them internal linkage, so they are unreachable from - * the library's other files and cannot collide with a same-named helper - * in one of them. + * The built-in defaults and the path normalization applied to the + * directories a context resolves. An unnamed namespace gives them + * internal linkage, so they are unreachable from the library's other + * files and cannot collide with a same-named helper in one of them. */ namespace { /** Configuration directory used when no override is supplied. */ @@ -50,30 +47,6 @@ namespace { /** Log file used when configuration enables logging without naming a path. */ const char* DEFAULT_LOG_FILE = "/var/log/dpm/dpm.log"; - /** - * @brief Strips leading and trailing whitespace - * - * Applied to every configuration line, section name, key, and value, - * so that surrounding spaces and the line's terminator never reach - * the configuration store. - * - * @param s The string to trim - * @return The trimmed string; empty when the input is entirely whitespace - */ - std::string trim(const std::string& s) { - const char* ws = " \t\r\n\f\v"; - - size_t start = s.find_first_not_of(ws); - - // Every character is whitespace, so nothing survives trimming. - if (start == std::string::npos) { - return ""; - } - - size_t end = s.find_last_not_of(ws); - return s.substr(start, end - start + 1); - } - /** * @brief Appends a trailing slash when one is absent * @@ -91,132 +64,6 @@ namespace { return s; } - /** - * @brief Interprets a configured boolean value - * - * Accepts the spellings a configuration file is likely to carry, in - * any case. A value that matches none of them leaves the setting at - * the caller's fallback rather than guessing. - * - * @param v The configured value - * @param fallback Returned when the value matches no known spelling - * @return The interpreted boolean, or the fallback - */ - bool parse_bool(const std::string& v, bool fallback) { - // Compare case-insensitively by folding a copy to lowercase. - std::string lower; - for (char c : v) { - lower += static_cast(std::tolower(static_cast(c))); - } - - if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") { - return true; - } - if (lower == "false" || lower == "no" || lower == "0" || lower == "off") { - return false; - } - - return fallback; - } - - /** - * @brief Maps a configured log level name to its numeric level - * - * The comparison is exact and case-sensitive, matching the spelling - * the shipped core.conf uses. - * - * @param v The level name read from configuration - * @param fallback Returned when the name is unrecognized - * @return A DPM_LOG_* value, or the fallback - */ - int level_from_string(const std::string& v, int fallback) { - if (v == "FATAL") { return DPM_LOG_FATAL; } - if (v == "ERROR") { return DPM_LOG_ERROR; } - if (v == "WARN") { return DPM_LOG_WARN; } - if (v == "INFO") { return DPM_LOG_INFO; } - if (v == "DEBUG") { return DPM_LOG_DEBUG; } - return fallback; - } - - /** - * @brief Names a numeric log level for display - * - * Used to label each message on the console and in the log file. - * - * @param level A DPM_LOG_* value - * @return The level's name, or "UNKNOWN" for a value outside the set - */ - const char* level_name(int level) { - switch (level) { - case DPM_LOG_FATAL: return "FATAL"; - case DPM_LOG_ERROR: return "ERROR"; - case DPM_LOG_WARN: return "WARN"; - case DPM_LOG_INFO: return "INFO"; - case DPM_LOG_DEBUG: return "DEBUG"; - default: return "UNKNOWN"; - } - } - - /** - * @brief Parses one configuration file into the context's store - * - * Reads a sectioned key/value file. Keys appearing before any - * section header belong to the section named "main". Blank lines, - * lines opening with '#' or ';', and lines carrying no '=' are - * skipped, so a malformed line costs that line alone. - * - * A file that cannot be opened contributes nothing and is not an - * error: configuration is optional, and defaults cover its absence. - * - * @param ctx The context whose configuration store receives the values - * @param file Path of the file to read - * @param module_name The namespace the values are filed under, which - * is the file's name without its extension - */ - void parse_config_file(dpm_ctx* ctx, const fs::path& file, - const std::string& module_name) { - std::ifstream in(file); - if (!in.is_open()) { - return; - } - - std::string line; - - // Keys read before the first section header belong to "main". - std::string section = "main"; - - while (std::getline(in, line)) { - line = trim(line); - - // Blank line or comment. - if (line.empty() || line[0] == '#' || line[0] == ';') { - continue; - } - - // Section header: everything after it files under this name. - if (line.front() == '[' && line.back() == ']') { - std::string name = trim(line.substr(1, line.length() - 2)); - section = name.empty() ? "main" : name; - continue; - } - - // Anything without a separator is not a key/value pair. - size_t eq = line.find('='); - if (eq == std::string::npos) { - continue; - } - - std::string key = trim(line.substr(0, eq)); - std::string value = trim(line.substr(eq + 1)); - - // A separator with nothing before it names no key. - if (key.empty()) { - continue; - } - - ctx->config[module_name][section][key] = value; - } - } } // namespace namespace dpm_core { @@ -231,39 +78,6 @@ namespace dpm_core { ctx->last_error = msg; } } - - /** - * @brief Loads every configuration file in the context's config directory - * - * Each .conf file becomes one configuration namespace named after the - * file, so core.conf fills the "core" namespace and mymodule.conf - * fills "mymodule". Files with any other extension are ignored. - * - * A missing or unreadable directory leaves the store empty, which - * leaves every setting at its default. - */ - void load_config_dir(dpm_ctx* ctx) { - std::error_code ec; - if (!fs::is_directory(ctx->config_dir, ec)) { - return; - } - - for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) { - // Stop on a directory read error rather than iterating further. - if (ec) { - break; - } - if (!entry.is_regular_file()) { - continue; - } - if (entry.path().extension() != ".conf") { - continue; - } - - // The namespace is the filename without its extension. - parse_config_file(ctx, entry.path(), entry.path().stem().string()); - } - } } // namespace dpm_core extern "C" { @@ -316,10 +130,10 @@ extern "C" { ctx->log_file = DEFAULT_LOG_FILE; if (const char* v = dpm_config_get(ctx, "core", "logging", "log_level")) { - ctx->log_level = level_from_string(v, DPM_LOG_INFO); + ctx->log_level = dpm_core::log_level_str2enum(v, DPM_LOG_INFO); } if (const char* v = dpm_config_get(ctx, "core", "logging", "write_to_log")) { - ctx->write_to_log = parse_bool(v, false); + ctx->write_to_log = dpm_core::parse_bool(v, false); } if (const char* v = dpm_config_get(ctx, "core", "logging", "log_file")) { ctx->log_file = v; @@ -369,83 +183,6 @@ extern "C" { delete ctx; } - /** - * @brief Reads a configured value - * - * Values are addressed by the three levels the configuration store - * is keyed on: the namespace a file supplied, a section within it, - * and a key within that section. A level that is absent yields NULL, - * which the caller reads as unset. - */ - const char* dpm_config_get(dpm_ctx* ctx, const char* module, - const char* section, const char* key) { - if (!ctx || !module || !section || !key) { - return nullptr; - } - - auto mod_it = ctx->config.find(module); - if (mod_it == ctx->config.end()) { - return nullptr; - } - - auto sec_it = mod_it->second.find(section); - if (sec_it == mod_it->second.end()) { - return nullptr; - } - - auto key_it = sec_it->second.find(key); - if (key_it == sec_it->second.end()) { - return nullptr; - } - - return key_it->second.c_str(); - } - - /** - * @brief Writes a message to the context's log targets - * - * Messages at or below the configured level are written; the rest - * are dropped. Errors and warnings go to stderr so they survive a - * caller redirecting stdout, and the remaining levels go to stdout - * as ordinary output. - */ - void dpm_log(dpm_ctx* ctx, int level, const char* message) { - if (!ctx || !message) { - return; - } - - // A level outside the known set is treated as ordinary reporting. - if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) { - level = DPM_LOG_INFO; - } - - if (level > ctx->log_level) { - return; - } - - if (level <= DPM_LOG_WARN) { - std::fprintf(stderr, "%s: %s\n", level_name(level), message); - } else { - std::fprintf(stdout, "%s\n", message); - } - - if (ctx->write_to_log) { - std::ofstream out(ctx->log_file, std::ios::app); - if (out.is_open()) { - char stamp[32]; - std::time_t now = std::time(nullptr); - std::strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", - std::localtime(&now)); - out << stamp << " [" << level_name(level) << "] " - << message << "\n"; - } else { - // Log-file trouble never fails the operation; disable the - // file target and carry on writing to the console. - ctx->write_to_log = false; - } - } - } - /** * @brief Reports the module directory this context resolved * diff --git a/src/core/logging.cpp b/src/core/logging.cpp new file mode 100644 index 0000000..aa37883 --- /dev/null +++ b/src/core/logging.cpp @@ -0,0 +1,112 @@ +/** + * @file logging.cpp + * @brief Writing a message to a context's log targets + * + * A context has two log targets: the console, which is always written, + * and a log file, written when configuration enables one. Both carry the + * same messages, and a message is written to both or to neither. + * + * Severity decides two things. A message above the context's configured + * level is dropped before either target is touched. A message at or + * below it is written, and its level picks the console stream: errors + * and warnings go to stderr so they survive a caller redirecting stdout, + * and the rest go to stdout as ordinary output. + * + * @copyright Copyright (c) 2026 SILO GROUP LLC + * @author Chris Punches + * + * Part of the Dark Horse Linux Package Manager (DPM) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "internal/context.hpp" + +#include +#include +#include + +/** + * @brief Helpers private to this translation unit + * + * The name a log level carries in output. An unnamed namespace gives it + * internal linkage, so it is unreachable from the library's other files + * and cannot collide with a same-named helper in one of them. + */ +namespace { + /** + * @brief Names a numeric log level for display + * + * Used to label each message on the console and in the log file. + * + * @param level A DPM_LOG_* value + * @return The level's name, or "UNKNOWN" for a value outside the set + */ + const char* log_level_enum2str(int level) { + switch (level) { + case DPM_LOG_FATAL: return "FATAL"; + case DPM_LOG_ERROR: return "ERROR"; + case DPM_LOG_WARN: return "WARN"; + case DPM_LOG_INFO: return "INFO"; + case DPM_LOG_DEBUG: return "DEBUG"; + default: return "UNKNOWN"; + } + } +} // namespace + +extern "C" { + /** + * @brief Writes a message to the context's log targets + * + * Messages at or below the configured level are written; the rest + * are dropped. Errors and warnings go to stderr so they survive a + * caller redirecting stdout, and the remaining levels go to stdout + * as ordinary output. + */ + void dpm_log(dpm_ctx* ctx, int level, const char* message) { + if (!ctx || !message) { + return; + } + + // A level outside the known set is treated as ordinary reporting. + if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) { + level = DPM_LOG_INFO; + } + + if (level > ctx->log_level) { + return; + } + + if (level <= DPM_LOG_WARN) { + std::fprintf(stderr, "%s: %s\n", log_level_enum2str(level), message); + } else { + std::fprintf(stdout, "%s\n", message); + } + + if (ctx->write_to_log) { + std::ofstream out(ctx->log_file, std::ios::app); + if (out.is_open()) { + char stamp[32]; + std::time_t now = std::time(nullptr); + std::strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", + std::localtime(&now)); + out << stamp << " [" << log_level_enum2str(level) << "] " + << message << "\n"; + } else { + // Log-file trouble never fails the operation; disable the + // file target and carry on writing to the console. + ctx->write_to_log = false; + } + } + } +} /* extern "C" */