odygrd / quill

Asynchronous Low Latency C++ Logging Library
MIT License
1.55k stars 158 forks source link

Abstraction #548

Closed developer3067 closed 1 month ago

developer3067 commented 2 months ago

Hi,

Is there anyway to make quill dependency injection friendly? For example if I want to create an interface e.g. ILogService and then use quill in an implementation e.g. LogService and inject ILogService* log_service into other classes, since quill uses macros like LOG_INFO this doesn't seem to be possible.

odygrd commented 2 months ago

hey,

Quill uses macros for logging to avoid performance issues associated with argument evaluation for unused log levels (e.g., debug). If logging were implemented as a function, even empty functions would require evaluating arguments for any log level, leading to potential performance degradation.

Additionally, Quill creates metadata as constexpr for performance reasons. This approach allows for efficient logging without sacrificing speed.

While you could wrap Quill under an interface if you only need to pass pre-formatted log messages (like "{}", string), doing so would mean sacrificing many of the benefits Quill provides, such as async formatting

odygrd commented 1 month ago
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"

#include <iostream>
#include <string>
#include <utility>

#define MY_LOG(log_service, log_level, message)                                                    \
  if (log_level >= log_service->get_log_level())                                                   \
  {                                                                                                \
    log_service->log(log_level, message);                                                          \
  }

enum LogLevel
{
  DEBUG,
  INFO
};

class ILogService
{
public:
  virtual void log(LogLevel log_level, std::string const& message) = 0;
  virtual void set_log_level(LogLevel log_level) = 0;
  LogLevel get_log_level() const noexcept { return _log_level; }

protected:
  LogLevel _log_level;
};

class LogService : public ILogService
{
public:
  LogService()
  {
    quill::BackendOptions backend_options;
    quill::Backend::start(backend_options);

    quill::PatternFormatterOptions pfo;
    pfo.format_pattern = "%(time) [%(thread_id)] LOG_%(log_level:<9) %(logger:<12) %(message)";
    auto console_sink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
    logger = quill::Frontend::create_or_get_logger("root", console_sink, pfo);
  }

  void set_log_level(LogLevel log_level)
  {
    _log_level = log_level;

    if (log_level == LogLevel::INFO)
    {
      logger->set_log_level(quill::LogLevel::Info);
    }
    else if (log_level == LogLevel::DEBUG)
    {
      logger->set_log_level(quill::LogLevel::Debug);
    }
  }

  void log(LogLevel log_level, std::string const& message)
  {
    quill::LogLevel level;
    if (log_level == LogLevel::INFO)
    {
      level = quill::LogLevel::Info;
    }
    else
    {
      level = quill::LogLevel::Debug;
    }

    QUILL_LOG_DYNAMIC(logger, level, "{}", message);
  }

private:
  quill::Logger* logger;
};

int main()
{
  LogService ls;
  ls.set_log_level(LogLevel::DEBUG);
  ILogService* ils = &ls;

  MY_LOG(ils, LogLevel::DEBUG, "test");
  MY_LOG(ils, LogLevel::INFO, fmtquill::format("The answer is {} and {}", 33, 13));
}

You could do that, but you'll lose formatting outside the hotpath, as well as file, line, and function information, which isn't ideal. Probably you can manually inject the file/line info in the formatted string message yourself in the log() function if you need them. For optimal performance, I recommend logging directly through the provided macros. If you need to support multiple logging libraries, consider wrapping their macros in your own, allowing you to switch the underlying logic via a compiler flag, for example.