Customizable `libmamba` logging implementation

- new logging system for users to plug-in a log handler which will receive
  log records from `libmamba` and do whatever appropriate logging impl
  they want with it (this is separate from the "console" output, only
  concerns logging);
- provides a `spdlog`-based log handler, which should do what previous
  `libmamba` versions did;
- `Context` can be initialized with a provided log handler;
- By default, if no provided log handler and logging is enabled,
  `Context` will setup a `spdlog`-based log handler;

(more to come: stdlib log handler, noop log handler, test log handler)

Note that this is a stepping stone towards removing `spdlog` as dependency
of `libmamba` (see also #3945)
This commit is contained in:
Klaim (Joël Lamotte) 2025-06-03 18:05:19 +02:00 committed by Joël Lamotte (Klaim)
parent d5c2a83932
commit 11641d374e
20 changed files with 751 additions and 320 deletions

View File

@ -229,6 +229,8 @@ set(
${LIBMAMBA_SOURCE_DIR}/core/history.cpp
${LIBMAMBA_SOURCE_DIR}/core/link.cpp
${LIBMAMBA_SOURCE_DIR}/core/link.hpp
${LIBMAMBA_SOURCE_DIR}/core/logging.cpp
${LIBMAMBA_SOURCE_DIR}/core/logging_spdlog.cpp
${LIBMAMBA_SOURCE_DIR}/core/menuinst.cpp
${LIBMAMBA_SOURCE_DIR}/core/output.cpp
${LIBMAMBA_SOURCE_DIR}/core/package_cache.cpp
@ -377,6 +379,8 @@ set(
${LIBMAMBA_INCLUDE_DIR}/mamba/core/fsutil.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/history.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/invoke.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/logging.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/logging_spdlog.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/menuinst.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/output.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/package_cache.hpp

View File

@ -9,16 +9,7 @@
namespace mamba
{
enum class log_level
{
trace,
debug,
info,
warn,
err,
critical,
off
};
}
#endif

View File

@ -14,9 +14,9 @@
#include "mamba/core/common_types.hpp"
#include "mamba/core/context_params.hpp"
#include "mamba/core/logging.hpp"
#include "mamba/core/palette.hpp"
#include "mamba/core/subdir_parameters.hpp"
#include "mamba/core/tasksync.hpp"
#include "mamba/download/mirror_map.hpp"
#include "mamba/download/parameters.hpp"
#include "mamba/fs/filesystem.hpp"
@ -68,6 +68,7 @@ namespace mamba
{
bool enable_logging = false;
bool enable_signal_handling = false;
logging::AnyLogHandler log_handler;
};
// Context singleton class
@ -77,16 +78,12 @@ namespace mamba
static void use_default_signal_handler(bool val);
struct OutputParams
{
int verbosity{ 0 };
log_level logging_level{ log_level::warn };
struct OutputParams : LoggingParams
{
bool json{ false };
bool quiet{ false };
std::string log_pattern{ "%^%-9!l%-8n%$ %v" };
std::size_t log_backtrace{ 0 };
};
struct GraphicsParams
@ -293,14 +290,6 @@ namespace mamba
specs::AuthenticationDataBase m_authentication_info;
bool m_authentication_infos_loaded = false;
class ScopedLogger;
std::vector<ScopedLogger> loggers;
std::shared_ptr<Logger> main_logger();
void add_logger(std::shared_ptr<Logger>);
TaskSynchronizer tasksync;
// Enables the provided context setup signal handling.
// This function must be called only for one Context in the lifetime of the program.
@ -308,7 +297,7 @@ namespace mamba
// Enables the provided context to drive the logging system.
// This function must be called only for one Context in the lifetime of the program.
void enable_logging();
void enable_logging(logging::AnyLogHandler log_handler);
};

View File

@ -12,6 +12,7 @@
#include <vector>
#include "mamba/core/context.hpp"
#include "mamba/core/progress_bar.hpp"
#include "mamba/core/package_fetcher.hpp"
#include "mamba/download/downloader.hpp"

View File

@ -0,0 +1,250 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_CORE_LOGGING_HPP
#define MAMBA_CORE_LOGGING_HPP
#include <concepts>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace mamba
{
enum class log_level
{
trace,
debug,
info,
warn,
err,
critical,
off
};
struct LoggingParams
{
int verbosity{ 0 };
log_level logging_level{ log_level::warn };
std::string log_pattern{ "%^%-9!l%-8n%$ %v" }; // IS THIS SPECIFIC TO spdlog???
std::size_t log_backtrace{ 0 };
};
enum class log_source // "source" isnt the best way to put it, maybe channel? component? sink?
{
libmamba,
libcurl,
libsolv
};
inline auto name_of(log_source source)
{
switch (source)
{
case log_source::libmamba:
return "libmamba";
case log_source::libcurl:
return "libcurl";
case log_source::libsolv:
return "libsolv";
}
// TODO(c++23): std::unreachable();
return "";
}
inline auto all_log_sources() -> std::vector<log_source> // TODO: do better :|
{
return { log_source::libmamba, log_source::libcurl, log_source::libsolv };
}
namespace logging
{
struct LogRecord
{
std::string message;
log_level level;
log_source source;
};
// NOTE: it might make more sense to talka bout sinks than sources when it comes to the
// implementation
template <class T>
concept LogHandler = std::equality_comparable<T> //
&& requires(
T& handler,
const T& const_handler //
,
LoggingParams params,
std::vector<log_source> sources,
LogRecord log_record //
,
std::optional<log_source> source // no value means all sources
) {
handler.start_log_handling(params, sources); //
handler.stop_log_handling(); //
handler.set_log_level(params.logging_level); //
handler.set_params(std::as_const(params)); //
handler.log(log_record); //
handler.log_stacktrace(); // log stacktrace in all sources
handler.log_stacktrace(source); // log stacktrace only in
// specific source
handler.log_stacktrace_no_guards(); // log stacktrace in all
// sources
handler.log_stacktrace_no_guards(source); // log stacktrace
// only in specific
// source
handler.flush(); // flush all
handler.flush(std::optional<log_source>{}); // flush only a
// specific source
};
class AnyLogHandler
{
public:
AnyLogHandler();
~AnyLogHandler();
template <class T>
requires(not std::is_same_v<std::remove_cvref_t<T>, AnyLogHandler>) and LogHandler<T>
AnyLogHandler(T&& handler);
template <class T>
requires(not std::is_same_v<std::remove_cvref_t<T>, AnyLogHandler>) and LogHandler<T>
AnyLogHandler(T* handler_ptr);
template <class T>
requires(not std::is_same_v<std::remove_cvref_t<T>, AnyLogHandler>) and LogHandler<T>
AnyLogHandler& operator=(T&& new_handler);
template <class T>
requires(not std::is_same_v<std::remove_cvref_t<T>, AnyLogHandler>) and LogHandler<T>
AnyLogHandler& operator=(T* new_handler_ptr);
auto start_log_handling(LoggingParams params, std::vector<log_source> sources) -> void;
auto stop_log_handling() -> void;
auto set_log_level(log_level new_level) -> void;
auto set_params(LoggingParams new_params) -> void;
auto log(LogRecord record) noexcept -> void;
auto log_stacktrace(std::optional<log_source> source = {}) noexcept -> void;
auto log_stacktrace_no_guards(std::optional<log_source> source = {}) noexcept -> void;
auto flush(std::optional<log_source> source = {}) noexcept -> void;
auto operator==(const AnyLogHandler& other) const noexcept -> bool;
template <class T>
requires(not std::is_same_v<std::remove_cvref_t<T>, AnyLogHandler>) and LogHandler<T>
auto operator==(const T& other) const noexcept -> bool;
auto has_value() const noexcept -> bool;
explicit operator bool() const noexcept
{
return has_value();
}
private:
struct Interface
{
virtual ~Interface() = 0;
};
};
static_assert(LogHandler<AnyLogHandler>); // NEEDED? AnyLogHandler must not recursively
// host itself
// not thread-safe
auto set_log_handler(AnyLogHandler handler, std::optional<LoggingParams> maybe_params = {})
-> AnyLogHandler;
// not thread-safe
auto get_log_handler() -> const AnyLogHandler&;
// thread-safe
auto set_log_level(log_level new_level) -> log_level;
// thread-safe, value immediately obsolete
auto get_log_level() noexcept -> log_level;
// thread-safe, value immediately obsolete
auto get_logging_params() noexcept -> LoggingParams;
// thread-safe
auto set_logging_params(LoggingParams new_params);
// as thread-safe as handler's implementation
auto log(LogRecord record) noexcept -> void;
// as thread-safe as handler's implementation
auto log_stacktrace(std::optional<log_source> source = {}) noexcept -> void;
// as thread-safe as handler's implementation
auto flush_logs(std::optional<log_source> source = {}) noexcept -> void;
// as thread-safe as handler's implementation
auto log_stacktrace_no_guards(std::optional<log_source> source = {}) noexcept -> void;
///////////////////////////////////////////////////////
// MIGT DISAPPEAR SOON
class MessageLogger
{
public:
MessageLogger(log_level level);
~MessageLogger();
std::stringstream& stream()
{
// Keep this implementation inline for performance reasons.
return m_stream;
}
static void activate_buffer();
static void deactivate_buffer();
static void print_buffer(std::ostream& ostream);
private:
log_level m_level;
std::stringstream m_stream;
static void emit(const std::string& msg, const log_level& level);
};
}
}
#undef LOG
#undef LOG_TRACE
#undef LOG_DEBUG
#undef LOG_INFO
#undef LOG_WARNING
#undef LOG_ERROR
#undef LOG_CRITICAL
#define LOG(severity) mamba::logging::MessageLogger(severity).stream()
#define LOG_TRACE LOG(mamba::log_level::trace)
#define LOG_DEBUG LOG(mamba::log_level::debug)
#define LOG_INFO LOG(mamba::log_level::info)
#define LOG_WARNING LOG(mamba::log_level::warn)
#define LOG_ERROR LOG(mamba::log_level::err)
#define LOG_CRITICAL LOG(mamba::log_level::critical)
#endif

View File

@ -0,0 +1,57 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_CORE_LOGGING_SPDLOG_HPP
#define MAMBA_CORE_LOGGING_SPDLOG_HPP
#include <memory>
#include <mamba/core/tasksync.hpp>
#include <mamba/core/logging.hpp>
#include <spdlog/common.h>
namespace mamba
{
// THINK: add namespace?
inline auto convert_log_level(log_level l) -> spdlog::level::level_enum
{
return static_cast<spdlog::level::level_enum>(l);
}
class LogHandler_spdlog
{
public:
LogHandler_spdlog();
~LogHandler_spdlog();
auto start_log_handling(LoggingParams params, std::vector<log_source> sources) -> void;
auto stop_log_handling() -> void;
auto set_log_level(log_level new_level) -> void;
auto set_params(LoggingParams new_params) -> void;
auto log(logging::LogRecord record) -> void;
auto log_stacktrace(std::optional<log_source> source = {}) -> void;
auto log_stacktrace_no_guards(std::optional<log_source> source = {}) -> void;
auto flush(std::optional<log_source> source = {}) -> void;
auto operator==(const LogHandler_spdlog& other) const noexcept -> bool;
private:
struct Impl;
std::unique_ptr<Impl> pimpl;
};
static_assert(logging::LogHandler<LogHandler_spdlog>);
}
#endif

View File

@ -16,7 +16,7 @@
#include <fmt/color.h>
#include <nlohmann/json.hpp>
#include "mamba/core/common_types.hpp"
#include "mamba/core/logging.hpp"
#include "mamba/core/progress_bar.hpp"
namespace mamba
@ -166,43 +166,6 @@ namespace mamba
static void clear_singleton();
};
class MessageLogger
{
public:
MessageLogger(log_level level);
~MessageLogger();
std::stringstream& stream();
static void activate_buffer();
static void deactivate_buffer();
static void print_buffer(std::ostream& ostream);
private:
log_level m_level;
std::stringstream m_stream;
static void emit(const std::string& msg, const log_level& level);
};
} // namespace mamba
#undef LOG
#undef LOG_TRACE
#undef LOG_DEBUG
#undef LOG_INFO
#undef LOG_WARNING
#undef LOG_ERROR
#undef LOG_CRITICAL
#define LOG(severity) mamba::MessageLogger(severity).stream()
#define LOG_TRACE LOG(mamba::log_level::trace)
#define LOG_DEBUG LOG(mamba::log_level::debug)
#define LOG_INFO LOG(mamba::log_level::info)
#define LOG_WARNING LOG(mamba::log_level::warn)
#define LOG_ERROR LOG(mamba::log_level::err)
#define LOG_CRITICAL LOG(mamba::log_level::critical)
#endif // MAMBA_CORE_OUTPUT_HPP

View File

@ -4,9 +4,9 @@
#include <stdexcept>
#include "mamba/core/output.hpp"
#include <fmt/core.h>
#include "spdlog/spdlog.h"
#include "mamba/core/logging.hpp"
namespace mamba
{

View File

@ -12,6 +12,7 @@
#include "mamba/core/activation.hpp"
#include "mamba/core/context.hpp"
#include "mamba/core/shell_init.hpp"
#include "mamba/core/output.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/util/path_manip.hpp"

View File

@ -14,6 +14,7 @@
// TODO includes to be removed after moving some functions/structs around
#include "mamba/api/install.hpp" // other_pkg_mgr_spec
#include "mamba/core/context.hpp"
#include "mamba/core/output.hpp"
#include "mamba/core/util.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/util/environment.hpp"

View File

@ -8,13 +8,11 @@
#include <fmt/ostream.h>
#include <fmt/ranges.h>
#include <spdlog/pattern_formatter.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
#include "mamba/api/configuration.hpp"
#include "mamba/core/context.hpp"
#include "mamba/core/execution.hpp"
#include "mamba/core/logging_spdlog.hpp"
#include "mamba/core/output.hpp"
#include "mamba/core/thread_utils.hpp"
#include "mamba/core/util.hpp"
@ -28,99 +26,6 @@
namespace mamba
{
class Logger : public spdlog::logger
{
public:
Logger(const std::string& name, const std::string& pattern, const std::string& eol);
void dump_backtrace_no_guards();
};
Logger::Logger(const std::string& name, const std::string& pattern, const std::string& eol)
: spdlog::logger(name, std::make_shared<spdlog::sinks::stderr_color_sink_mt>())
{
auto f = std::make_unique<spdlog::pattern_formatter>(
pattern,
spdlog::pattern_time_type::local,
eol
);
set_formatter(std::move(f));
}
void Logger::dump_backtrace_no_guards()
{
using spdlog::details::log_msg;
if (tracer_.enabled())
{
tracer_.foreach_pop(
[this](const log_msg& msg)
{
if (this->should_log(msg.level))
{
this->sink_it_(msg);
}
}
);
}
}
enum class logger_kind
{
normal_logger,
default_logger,
};
// Associate the registration of a logger to the lifetime of this object.
// This is used to help with making sure loggers are unregistered once
// their logical owner is destroyed.
class Context::ScopedLogger
{
std::shared_ptr<Logger> m_logger;
public:
explicit ScopedLogger(std::shared_ptr<Logger> new_logger, logger_kind kind = logger_kind::normal_logger)
: m_logger(std::move(new_logger))
{
assert(m_logger);
if (kind == logger_kind::default_logger)
{
spdlog::set_default_logger(m_logger);
}
else
{
spdlog::register_logger(m_logger);
}
}
~ScopedLogger()
{
if (m_logger)
{
spdlog::drop(m_logger->name());
}
}
std::shared_ptr<Logger> logger() const
{
assert(m_logger);
return m_logger;
}
ScopedLogger(ScopedLogger&&) = default;
ScopedLogger& operator=(ScopedLogger&&) = default;
ScopedLogger(const ScopedLogger&) = delete;
ScopedLogger& operator=(const ScopedLogger&) = delete;
};
spdlog::level::level_enum convert_log_level(log_level l)
{
return static_cast<spdlog::level::level_enum>(l);
}
namespace
{
std::atomic<bool> use_default_signal_handler_val{ true };
@ -139,16 +44,6 @@ namespace mamba
}
}
std::shared_ptr<Logger> Context::main_logger()
{
if (loggers.empty())
{
return {};
}
return loggers.front().logger();
}
void Context::enable_signal_handling()
{
if (use_default_signal_handler_val)
@ -157,22 +52,19 @@ namespace mamba
}
}
void Context::enable_logging()
void Context::enable_logging(logging::AnyLogHandler log_handler) // THINK: change name? start_logging?
{
loggers.clear(); // Make sure we work with a known set of loggers, first one is
// always the default one.
loggers.emplace_back(
std::make_shared<Logger>("libmamba", output_params.log_pattern, "\n"),
logger_kind::default_logger
);
MainExecutor::instance().on_close(tasksync.synchronized([&] { main_logger()->flush(); }));
loggers.emplace_back(std::make_shared<Logger>("libcurl", output_params.log_pattern, ""));
loggers.emplace_back(std::make_shared<Logger>("libsolv", output_params.log_pattern, ""));
spdlog::set_level(convert_log_level(output_params.logging_level));
if (not logging::get_log_handler())
{
if (log_handler)
{
logging::set_log_handler(std::move(log_handler), output_params);
}
else
{
logging::set_log_handler(LogHandler_spdlog(), output_params);
}
}
}
Context::Context(const ContextOptions& options)
@ -215,7 +107,7 @@ namespace mamba
if (options.enable_logging)
{
enable_logging();
enable_logging(options.log_handler);
}
}
@ -252,13 +144,13 @@ namespace mamba
this->output_params.logging_level = log_level::info;
break;
}
spdlog::set_level(convert_log_level(output_params.logging_level));
logging::set_log_level(output_params.logging_level);
}
void Context::set_log_level(log_level level)
{
output_params.logging_level = level;
spdlog::set_level(convert_log_level(level));
logging::set_log_level(level);
}
std::vector<std::string> Context::platforms() const
@ -422,10 +314,7 @@ namespace mamba
void Context::dump_backtrace_no_guards()
{
if (main_logger()) // REVIEW: is this correct?
{
main_logger()->dump_backtrace_no_guards();
}
logging::log_stacktrace_no_guards(log_source::libmamba);
}
} // namespace mamba

View File

@ -1,5 +1,7 @@
#include "mamba/core/download_progress_bar.hpp"
#include "mamba/core/output.hpp"
#include "progress_bar_impl.hpp"
namespace mamba

View File

@ -1,6 +1,6 @@
#include "mamba/core/error_handling.hpp"
#include "spdlog/spdlog.h"
#include "mamba/core/logging.hpp"
namespace mamba
{
@ -10,7 +10,7 @@ namespace mamba
{
if (ec == mamba_error_code::internal_failure)
{
spdlog::dump_backtrace();
logging::log_stacktrace();
}
}

View File

@ -0,0 +1,209 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <mutex>
#include <utility>
#include <vector>
#include <mamba/core/context.hpp>
#include <mamba/core/error_handling.hpp>
#include <mamba/core/execution.hpp>
#include <mamba/core/logging.hpp>
#include <mamba/core/output.hpp> // TODO: remove
#include <mamba/core/util.hpp>
#include <mamba/util/synchronized_value.hpp>
namespace mamba::logging
{
namespace // TODO: STATIC INIT FIASCO!!!
{
util::synchronized_value<LoggingParams> logging_params;
// IMPRTANT NOTE:
// The handler MUST NOT be protected from concurrent calls at this level
// as that would add high performance cost to logging from multiple threads.
// Instead, we expect the implementation to handle concurrent calls correctly
// in a thread-safe manner which is appropriate for this implementation.
//
// There are many ways to implement such handlers, including with a mutex,
// various mutex types, through a concurrent lock-free queue of logging record feeding
// a specific thread responsible for the output and file writing, etc.
// There are too many ways that we can't predict and each impl will have it's own
// best strategy.
//
// So instead of protecting at this level, we require the implementation of the handler
// to be thread-safe, whatever the means. We can't enforce that property and can only
// require it with the documentation which should guide the implementers anyway.
AnyLogHandler current_log_handler;
// FIXME: maybe generalize and move in synchronized_value.hpp
template<std::default_initializable T, typename U, typename... OtherArgs>
requires std::assignable_from<T&, U>
auto
synchronize_with_value(util::synchronized_value<T, OtherArgs...>& sv, const std::optional<U>& new_value)
{
auto synched_value = sv.synchronize();
if (new_value)
{
*synched_value = *new_value;
}
return synched_value;
}
}
auto set_log_handler(AnyLogHandler new_handler, std::optional<LoggingParams> maybe_params)
-> AnyLogHandler
{
if (current_log_handler)
{
current_log_handler.stop_log_handling();
}
auto previous_handler = std::exchange(current_log_handler, std::move(new_handler));
auto params = synchronize_with_value(logging_params, maybe_params);
if (current_log_handler)
{
current_log_handler.start_log_handling(*params, all_log_sources());
}
}
auto get_log_handler() -> const AnyLogHandler&
{
return current_log_handler;
}
auto set_log_level(log_level new_level) -> log_level
{
auto synched_params = logging_params.synchronize();
const auto previous_level = synched_params->logging_level;
synched_params->logging_level = new_level;
current_log_handler.set_log_level(synched_params->logging_level);
return previous_level;
}
auto get_log_level() noexcept -> log_level
{
return logging_params->logging_level;
}
auto get_logging_params() noexcept -> LoggingParams
{
return logging_params.value();
}
auto set_logging_params(LoggingParams new_params)
{
logging_params = std::move(new_params);
}
auto log(LogRecord record) noexcept -> void
{
current_log_handler.log(std::move(record));
}
auto log_stacktrace(std::optional<log_source> source) noexcept -> void
{
current_log_handler.log_stacktrace(std::move(source));
}
auto flush_logs(std::optional<log_source> source) noexcept -> void
{
current_log_handler.flush(std::move(source));
}
auto log_stacktrace_no_guards(std::optional<log_source> source) noexcept -> void
{
current_log_handler.log_stacktrace_no_guards(std::move(source));
}
///////////////////////////////////////////////////////////////////
// AnyLogHandler
AnyLogHandler::AnyLogHandler() = default;
AnyLogHandler::~AnyLogHandler() = default;
///////////////////////////////////////////////////////////////////
// MessageLogger
struct MessageLoggerData
{
static std::mutex m_mutex;
static bool use_buffer;
static std::vector<std::pair<std::string, log_level>> m_buffer;
};
MessageLogger::MessageLogger(log_level level)
: m_level(level)
{
}
MessageLogger::~MessageLogger()
{
if (!MessageLoggerData::use_buffer && Console::is_available())
{
emit(m_stream.str(), m_level);
}
else
{
const std::lock_guard<std::mutex> lock(MessageLoggerData::m_mutex);
MessageLoggerData::m_buffer.push_back({ m_stream.str(), m_level });
}
}
void MessageLogger::emit(const std::string& msg, const log_level& level)
{
// THINK: maybe remove as much locals as possible to enable optimizations with temporaries
// TODO: use fmt or std::format to do the space prepend
const auto secured_message = Console::hide_secrets(msg);
const auto formatted_message = prepend(secured_message, "", std::string(4, ' ').c_str());
logging::log(LogRecord{
.message = formatted_message, //
.level = level, //
.source = log_source::libmamba //
});
if (level == log_level::critical and get_log_level() != log_level::off)
{
log_stacktrace();
}
}
void MessageLogger::activate_buffer()
{
MessageLoggerData::use_buffer = true;
}
void MessageLogger::deactivate_buffer()
{
MessageLoggerData::use_buffer = false;
}
void MessageLogger::print_buffer(std::ostream& /*ostream*/)
{
decltype(MessageLoggerData::m_buffer) tmp;
{
const std::lock_guard<std::mutex> lock(MessageLoggerData::m_mutex);
MessageLoggerData::m_buffer.swap(tmp);
}
for (const auto& [msg, level] : tmp)
{
emit(msg, level);
}
// TODO impl commented
/*spdlog::apply_all([&](std::shared_ptr<spdlog::logger> l) { l->flush(); });*/
flush_logs();
}
}

View File

@ -0,0 +1,159 @@
// Copyright (c) 2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <mamba/core/logging_spdlog.hpp>
#include <vector>
#include <mutex>
#include <mamba/core/context.hpp>
#include <mamba/core/output.hpp> // TODO: remove
#include <mamba/core/util.hpp>
#include <mamba/core/execution.hpp>
#include <mamba/core/tasksync.hpp>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
namespace mamba
{
// FIXME: merge scoped logger with this type, they are the samem, scopepd logger as introduced to patch logger
class Logger : public spdlog::logger
{
public:
Logger(const std::string& name, const std::string& pattern, const std::string& eol);
void dump_backtrace_no_guards();
};
Logger::Logger(
const std::string& name,
const std::string& pattern,
const std::string& eol
)
: spdlog::logger(name, std::make_shared<spdlog::sinks::stderr_color_sink_mt>())
{
auto f = std::make_unique<spdlog::pattern_formatter>(
pattern,
spdlog::pattern_time_type::local,
eol
);
set_formatter(std::move(f));
}
void Logger::dump_backtrace_no_guards()
{
using spdlog::details::log_msg;
if (tracer_.enabled())
{
tracer_.foreach_pop(
[this](const log_msg& msg)
{
if (this->should_log(msg.level))
{
this->sink_it_(msg);
}
}
);
}
}
enum class logger_kind
{
normal_logger,
default_logger,
};
// Associate the registration of a logger to the lifetime of this object.
// This is used to help with making sure loggers are unregistered once
// their logical owner is destroyed.
class ScopedLogger
{
std::shared_ptr<Logger> m_logger;
public:
explicit ScopedLogger(
std::shared_ptr<Logger> new_logger,
logger_kind kind = logger_kind::normal_logger
)
: m_logger(std::move(new_logger))
{
assert(m_logger);
if (kind == logger_kind::default_logger)
{
spdlog::set_default_logger(m_logger);
}
else
{
spdlog::register_logger(m_logger);
}
}
~ScopedLogger()
{
if (m_logger)
{
spdlog::drop(m_logger->name());
}
}
std::shared_ptr<Logger> logger() const
{
assert(m_logger);
return m_logger;
}
ScopedLogger(ScopedLogger&&) = default;
ScopedLogger& operator=(ScopedLogger&&) = default;
ScopedLogger(const ScopedLogger&) = delete;
ScopedLogger& operator=(const ScopedLogger&) = delete;
};
struct LogHandler_spdlog::Impl
{
std::vector<ScopedLogger> loggers;
TaskSynchronizer tasksync;
};
LogHandler_spdlog::LogHandler_spdlog() = default;
LogHandler_spdlog::~LogHandler_spdlog() = default;
auto LogHandler_spdlog::start_log_handling(const LoggingParams params, std::vector<log_source> sources) -> void
{
pimpl = std::make_unique<Impl>();
const auto main_source = sources.front();
sources.pop_back();
pimpl->loggers.emplace_back(
std::make_shared<Logger>(name_of(main_source), params.log_pattern, "\n"),
logger_kind::default_logger
);
MainExecutor::instance().on_close(
pimpl->tasksync.synchronized([this] { pimpl->loggers.front().logger()->flush(); })
);
for (const auto source : sources)
{
pimpl->loggers.emplace_back(
std::make_shared<Logger>(name_of(source), params.log_pattern, "")
);
}
spdlog::set_level(convert_log_level(params.logging_level));
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019, QuantStack and Mamba Contributors
// Copyright (c) 2019-2025, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
@ -466,9 +466,9 @@ namespace mamba
{
auto new_progress_bar_manager = make_progress_bar_manager(mode);
new_progress_bar_manager->register_print_hook(Console::print_buffer);
new_progress_bar_manager->register_print_hook(MessageLogger::print_buffer);
new_progress_bar_manager->register_pre_start_hook(MessageLogger::activate_buffer);
new_progress_bar_manager->register_post_stop_hook(MessageLogger::deactivate_buffer);
new_progress_bar_manager->register_print_hook(logging::MessageLogger::print_buffer);
new_progress_bar_manager->register_pre_start_hook(logging::MessageLogger::activate_buffer);
new_progress_bar_manager->register_post_stop_hook(logging::MessageLogger::deactivate_buffer);
auto synched_data = p_data->m_synched_data.synchronize();
synched_data->progress_bar_manager = std::move(new_progress_bar_manager);
@ -553,92 +553,4 @@ namespace mamba
}
}
/*****************
* MessageLogger *
*****************/
static std::atomic<bool> message_logger_use_buffer;
using MessageLoggerBuffer = std::vector<std::pair<std::string, log_level>>;
static util::synchronized_value<MessageLoggerBuffer> message_logger_buffer;
MessageLogger::MessageLogger(log_level level)
: m_level(level)
, m_stream()
{
}
MessageLogger::~MessageLogger()
{
if (!message_logger_use_buffer && Console::is_available())
{
emit(m_stream.str(), m_level);
}
else
{
message_logger_buffer->push_back({ m_stream.str(), m_level });
}
}
void MessageLogger::emit(const std::string& msg, const log_level& level)
{
auto str = Console::hide_secrets(msg);
switch (level)
{
case log_level::critical:
SPDLOG_CRITICAL(prepend(str, "", std::string(4, ' ').c_str()));
if (Console::instance().context().output_params.logging_level != log_level::off)
{
spdlog::dump_backtrace();
}
break;
case log_level::err:
SPDLOG_ERROR(prepend(str, "", std::string(4, ' ').c_str()));
break;
case log_level::warn:
SPDLOG_WARN(prepend(str, "", std::string(4, ' ').c_str()));
break;
case log_level::info:
SPDLOG_INFO(prepend(str, "", std::string(4, ' ').c_str()));
break;
case log_level::debug:
SPDLOG_DEBUG(prepend(str, "", std::string(4, ' ').c_str()));
break;
case log_level::trace:
SPDLOG_TRACE(prepend(str, "", std::string(4, ' ').c_str()));
break;
default:
break;
}
}
std::stringstream& MessageLogger::stream()
{
return m_stream;
}
void MessageLogger::activate_buffer()
{
message_logger_use_buffer = true;
}
void MessageLogger::deactivate_buffer()
{
message_logger_use_buffer = false;
}
void MessageLogger::print_buffer(std::ostream& /*ostream*/)
{
MessageLoggerBuffer tmp;
message_logger_buffer->swap(tmp);
for (const auto& [msg, level] : tmp)
{
emit(msg, level);
}
spdlog::apply_all([&](std::shared_ptr<spdlog::logger> l) { l->flush(); });
}
} // namespace mamba

View File

@ -4,8 +4,7 @@
//
// The full license is in the file LICENSE, distributed with this software.
#include <spdlog/spdlog.h>
#include "mamba/core/logging.hpp"
#include "mamba/util/string.hpp"
#include "compression.hpp"
@ -77,9 +76,7 @@ namespace mamba::download
auto ret = ZSTD_decompressStream(p_stream, &output, &input);
if (ZSTD_isError(ret))
{
// This is temporary...
// TODO Remove dependency on spdlog after deciding on what to do with logging
spdlog::error("ZSTD decompression error: {}", ZSTD_getErrorName(ret));
LOG_ERROR << fmt::format("ZSTD decompression error: {}", ZSTD_getErrorName(ret));
return size + 1;
}
if (output.pos > 0)
@ -151,9 +148,7 @@ namespace mamba::download
int ret = BZ2_bzDecompress(&m_stream);
if (ret != BZ_OK && ret != BZ_STREAM_END)
{
// This is temporary...
// TODO Remove dependency on spdlog after deciding on what to do with logging
spdlog::error("Bzip2 decompression error: {}", ret);
LOG_ERROR << fmt::format("Bzip2 decompression error: {}", ret);
return size + 1;
}
@ -236,9 +231,7 @@ namespace mamba::download
auto ret = ZSTD_decompressStream(stream, &output, &input);
if (ZSTD_isError(ret))
{
// This is temporary...
// TODO Remove dependency on spdlog after deciding on what to do with logging
spdlog::error("ZSTD decompression error: {}", ZSTD_getErrorName(ret));
LOG_ERROR << fmt::format("ZSTD decompression error: {}", ZSTD_getErrorName(ret));
return size + 1;
}
if (output.pos > 0)
@ -268,9 +261,7 @@ namespace mamba::download
int ret = BZ2_bzDecompress(stream);
if (ret != BZ_OK && ret != BZ_STREAM_END)
{
// This is temporary...
// TODO Remove dependency on spdlog after deciding on what to do with logging
spdlog::error("Bzip2 decompression error: {}", ret);
LOG_ERROR << fmt::format("Bzip2 decompression error: {}", ret);
return size + 1;
}

View File

@ -288,24 +288,36 @@ namespace mamba::download
namespace
{
int
curl_debug_callback(CURL* /* handle */, curl_infotype type, char* data, size_t size, void* userptr)
curl_debug_callback(CURL* /* handle */, curl_infotype type, char* data, size_t size, void*)
{
auto* logger = reinterpret_cast<spdlog::logger*>(userptr);
std::string log;
static constexpr auto symbol_for = [](curl_infotype type) {
switch (type)
{
case CURLINFO_TEXT:
return "*";
case CURLINFO_HEADER_OUT:
return ">";
case CURLINFO_HEADER_IN:
return "<";
default:
return "";
};
};
switch (type)
{
case CURLINFO_TEXT:
log = Console::hide_secrets(std::string_view(data, size));
logger->info(fmt::format("* {}", log));
break;
case CURLINFO_HEADER_OUT:
log = Console::hide_secrets(std::string_view(data, size));
logger->info(fmt::format("> {}", log));
break;
case CURLINFO_HEADER_IN:
log = Console::hide_secrets(std::string_view(data, size));
logger->info(fmt::format("< {}", log));
{
const auto message = fmt::format("{} {}", symbol_for(type), Console::hide_secrets(std::string_view(data, size)));
logging::log({
.message = message,
.level = log_level::info,
.source = log_source::libcurl
});
break;
}
default:
// WARNING Using `hide_secrets` here will give a seg fault on linux,
// and other errors on other platforms
@ -377,9 +389,6 @@ namespace mamba::download
configure_handle_headers(params, auth_info);
auto logger = spdlog::get("libcurl");
p_handle->set_opt(CURLOPT_DEBUGFUNCTION, curl_debug_callback);
p_handle->set_opt(CURLOPT_DEBUGDATA, logger.get());
}
void DownloadAttempt::Impl::configure_handle_headers(
@ -1157,7 +1166,7 @@ namespace mamba::download
auto completion_callback = m_completion_map.find(msg.m_handle_id);
if (completion_callback == m_completion_map.end())
{
spdlog::error(
LOG_ERROR << std::format(
"Received DONE message from unknown target - running transfers left = {}",
still_running
);

View File

@ -12,6 +12,7 @@
#include "mamba/core/context.hpp"
#include "mamba/core/util.hpp"
#include "mamba/core/util_scope.hpp"
#include "mamba/fs/filesystem.hpp"
#include "spdlog/spdlog.h"

View File

@ -1023,11 +1023,13 @@ bind_submodule_impl(pybind11::module_ m)
decltype(Context::OutputParams::json) json,
decltype(Context::OutputParams::quiet) quiet) -> Context::OutputParams
{
return {
.verbosity = std::move(verbosity),
// TODO: improve this, see https://wg21.link/p2287 for the reason
auto params = Context::OutputParams{
.json = std::move(json),
.quiet = std::move(quiet),
};
params.verbosity = std::move(verbosity);
return params;
}
),
py::arg("verbosity") = default_output_params.verbosity,