G3log: Asynchronous logging the easy way

It is my privilege to present g3log, the successor of g2log that has been brewing since the creation of the g2log-dev.
G3log is just like g2log a “crash-safe asynchronous logger“. It is made to be simple to setup and easy to use with an appealing, non-cluttered API.
G3log is just like g2log blazing fast but with approximately 30% or faster LOG processing. G3log adds some important features:

  1. Completely safe to use across libraries, even libraries that are dynamically loaded at runtime
  2. Easy to customize with log receiving sinks. This is the major feature of g3log compared to g2log as it gives the users of g3log the ability to tweak it as they like without having to touch the logger’s source code.
  3. G3log provides faster log processing compared to g2log. The more LOG calling threads, the greater the performance difference in g3log’s favor.

G3log is just like g2log released as a public domain dedication. You can read more about that here: http://unlicense.org

So, don’t let slow logging bog your performance down. Do like developers, companies and research institutes are doing all over the globe, use g3log. Want to know more? Read on …

If you already know what you get from g2log then continue on to the new features of g3log. If you are new to g2log then you can first read a quick recap of what you get when using g2log/g3log.

G2log and G3log recap

g3log (and g2log) is an asynchronous logging utility made to be efficient and easy to use, understand, and modify. The reason for creating g2log and later g3log was simply that other logging utilities I researched were not good enough API-wise or efficiency-wise.

There are good, traditional, [#reasons] for using a synchronous logger. Unfortunately a traditional synchronous logger is just too slow for high performance code. G2log and g3log satisfy those #reasons while still being asynchronous

To get the essence of g3log it is only needed to read a few highlights:

  • It is a logging and design-by-contract framework.
  • Slow I/O access commonly associated with logging is done in FIFO order by one or several background threads
  • A LOG call returns immediately while the actual logging work is done in the background
  • Queued log entries are flushed to the log sinks at shutdown.
  • It is thread safe, so using it from multiple threads is completely fine.
  • It catches SIGSEGV and other fatal signals and logs them before exiting.
  • It is cross platform. For now, tested on Windows7, various Linux distributions and OSX. There is a small difference between the *nix and the Windows version. On Linux/OSX a caught fatal signal will generate a stack dump to the log.
  • g2log is used in commercial products as well as hobby projects since early 2011.
  • g3log is used in commercial products since fall of 2013.
  • The code is given for free as public domain. This gives the option to change, use, and do whatever with it, no strings attached.

Using g3log

g3log uses level-specific logging. This is done without slowing down the log-calling part of the software. Thanks to the concept of active object g3log gets asynchronous logging – the actual logging work with slow disk or network I/O access is done in one or several background threads

Example usage

Optional to use either streaming or printf-like syntax
Conditional LOG_IF as well as normal LOG calls are available. The default log levels are: DEBUG, INFO, WARNING, and FATAL.

LOG(INFO) << "streaming API is as easy as ABC or " << 123;

// The streaming API has a printf-like equivalent
LOGF(WARNING, "Printf-style syntax is also %s", "available");

Conditional logging

LOG_IF(DEBUG, small < large) << "Conditional logging is also available.

// The streaming API has a printf-like equivalent
LOGF_IF(INFO, small > large,
"Only expressions that are evaluated to true %s,
"will trigger a log call for the conditional API");

Design-by-Contract
CHECK(false) will trigger a “fatal” message. It will be logged, and then the application will exit. A LOG(FATAL) call is in essence the same as calling CHECK(false).

CHECK(false) << "triggers a FATAL message"
CHECKF(boolean_expression,"printf-api is also available");

Initialization

A typical scenario for using g3log would be as shown below. Immediately at start up, in the main function, g2::LogWorker is initialized with the default log-to-file sink.

// main.cpp
#include<g2log.hpp>
#include<g2logworker.hpp>
#include <std2_make_unique.hpp>

#include "CustomSink.h" // can be whatever

int main(int argc, char**argv) {
using namespace g2;
std::unique_ptr<LogWorker> logworker{ LogWorker::createWithNoSink() }; // 1
auto sinkHandle = logworker->addSink(std2::make_unique<CustomSink>(),  // 2
&CustomSink::ReceiveLogMessage); // 3
g2::initializeLogging(logworker.get());  // 4
  1. The LogWorker is created with no sinks.
  2. A sink is added and a sink handle with access to the sinks API is returned
  3. When adding a sink the default log function must be specified
  4. At initialization the logging must be initialized once to allow for LOG calls

G3log with sinks

Sinks are receivers of LOG calls. G3log comes with a default sink (the same as G2log uses) that saves LOG calls to file. A sink can be of any class type without restrictions as long as it can either receive a LOG message as a std::string or as a g2::LogMessageMover.

The std::string option will give pre-formatted LOG output. The g2::LogMessageMover is a wrapped struct that contains the raw data for custom handling and formatting in your own sink.

Using g2::LogMessage is easy:

// example from the default FileSink. It receives the LogMessage
//        and applies the default formatting with .toString()
void FileSink::fileWrite(LogMessageMover message) {
...
out << message.get().toString();
}

Sink Creation

When adding a custom sink a log receiving function must be specified, taking as argument a std::string for the default log formatting look or taking as argument a g2::LogMessage for your own custom made log formatting.

auto sinkHandle = logworker->addSink(std2::make_unique<CustomSink>(), // 1, 3
&CustomSink::ReceiveLogMessage); // 2
  1. A sink is owned by the G3log and is transferred to the logger wrapped in a std::unique_ptr
  2. The sink’s log receiving function is given as argument to LogWorker::addSink
  3. LogWorker::addSink returns a handle to the custom sink.

Calling the custom sink

All public functions of the custom sink are reachable through the handler.

// handle-to-sink calls are thread safe. The calls are executed asynchronously
std::future<void> received = sinkHandle->call(&CustomSink::Foo, some_param, other);

Code Examples

Example usage where a custom sink is added. A function is called though the sink handler to the actual sink object.

// main.cpp
#include<g2log.hpp>
#include<g2logworker.hpp>
#include <std2_make_unique.hpp>

#include "CustomSink.h"

int main(int argc, char**argv) {
using namespace g2;
std::unique_ptr<LogWorker> logworker{ LogWorker::createWithNoSink() };
auto sinkHandle = logworker->addSink(std2::make_unique<CustomSink>(),
&CustomSink::ReceiveLogMessage);

// initialize the logger before it can receive LOG calls
initializeLogging(logworker.get());
LOG(WARNING) << "This log call, may or may not happend before"
<< "the sinkHandle->call below";

// You can call in a thread safe manner public functions on your sink
// The call is asynchronously executed on your custom sink.
std::future<void> received = sinkHandle->call(&CustomSink::Foo,
param1, param2);

// If the LogWorker is initialized then at scope exit the g2::shutDownLogging() will be called.
// This is important since it protects from LOG calls from static or other entities that will go out of
// scope at a later time.
//
// It can also be called manually:
g2::shutDownLogging();
}

Example usage where a the default file logger is used and a custom sink is added

// main.cpp
#include<g2log.hpp>
#include<g2logworker.hpp>
#include <std2_make_unique.hpp>

#include "CustomSink.h"

int main(int argc, char**argv) {
using namespace g2;
auto defaultHandler = LogWorker::createWithDefaultLogger(argv[0],
path_to_log_file);

// logger is initialized
g2::initializeLogging(defaultHandler.worker.get());

LOG(DEBUG) << "Make log call, then add another sink";

defaultHandler.worker->addSink(std2::make_unique<CustomSink>(),
&CustomSink::ReceiveLogMessage);

...
}

It is easy to start using the logger anywhere in your code base.

// some_file.cpp
#include <g2log.hpp>
void SomeFunction() {
...
LOG(INFO) << "Hello World";
}

g3log API

In addition to the logging API: LOG, LOG_IF, CHECK (and the similar printf-calls) there are a few functions that are helpful for using and tweaking g3log.

Initialization

initializeLogging(...)

Dynamic log levels
I.e. disabling/enabling of log-levels at runtime. Enabling of this feature is done by a #define G2_DYNAMIC_LOGGING.

// g2loglevels.hpp
void setLogLevel(LEVELS level, bool enabled_status);
bool logLevel(LEVELS level); /

Sink Handling
See g2logworker.hpp for adding a custom sink, or accessing the default filesink.

std::unique_ptr<SinkHandle<T>> addSink(...)
static g2::DefaultFileLogger createWithDefaultLogger(...)
static std::unique_ptr<LogWorker> createWithNoSink();

Internal Functions
See g3log.hpp for several internal functions that usually don’t have to be used by the coder but can if using the g3log library to the fullest.

// will be called when the LogWorker goes out of scope
shutDownLogging()

// for unit testing, or customized fatal call handling
void changeFatalInitHandlerForUnitTesting(...)

Where to get it

Please see: https://github.com/KjellKod/g3log

Enjoy
Kjell (a.k.a KjellKod)

About kjellkod

Software Engineer by trade, (former?) Martial Artist and Champion by strong will and small skill... and a Swede by nationality :)
This entry was posted in g3log. Bookmark the permalink.

7 Responses to G3log: Asynchronous logging the easy way

  1. minorfs says:

    Would be interesting if you could explore the possibilities of an additional DI style API for this library. IMO a marriage of g3log and libKISSLog could yield the perfect C++ logging library. If you could add a singleton/macro-free entrance to the API that allows for friendly DI, the way libKISSLog does, combined with the asynchronous logging that G3log offers, you could IMHO make all other logging libraries obsolete.

    As a side note, could you maybe write a bit about the fact that buffers aren’t infinite in size, and how G3log handles that. Whenever I bring up the actor model, people seem to jump to the conclusion that it means that congestion automatically leads to std::bad_alloc nightmares. Could you elaborate on how (assuming that you have) you have made sure that does not happen in G3log?

    • kjellkod says:

      Thanks for the praise 🙂

      You have my email? Contact me and let’s juggle some ideas about your suggestion.

      From a coder usage perspective I think the simple usage of g2log is hard to beat together with its RAII shut down handling.

      There are use cases that it doesn’t handle out-of-the box. I.e logging per file, module etc (one log file per “instance”). It could be done through custom sinks but I’m sure there are better ways.

      About the actor model. I’ve used the actor model and the active object throughout my whole career. It’s great for making threaded code easier to use and harder to misuse.

      Sometimes it’s definitely important to have bounded queues to avoid uncontrolled memory usage. For high performing actor models I’ve seen ZMQ bounded queues. For small-footprint embedded systems I’ve seen lock-free bounded queues.

      In the case of g2log or g3log a std::queue is used, internally that is a std::deque. It’s unbounded but much more memory tolerant than a std::vector. Internally the queue is wrapped inside the shared_queue.hpp.

      It’s easy to replace the internal queue for something else if the need would arise.

      For a logger It would be an exceptional case if logging is done so frequently that the bg thread can’t keep up and we run into memory issues like bad alloc.

  2. Mahendrakumar says:

    where this logs will be generated ? what if I want to supply path to g3log ?

  3. Mahendrakumar says:

    If I initialize and log in sample cpp program having main method, it works properly but when I integrated in my dll project where there is no main method but I have method which I call firstly where i have followed the same code as explained in “https://github.com/KjellKod/g3log/issues/88#issuecomment-219030139”. It generates the log properly but immediately my java’s GUI system get crashed and I get the same error. Please help me to get rid of this.

    namespace {
    static std::once_flag g_initLogger;
    static std::unique_ptr g_logger;
    static std::unique_ptr g_loggerHandle;
    } // namespace

    void InitializeLogging(const std::string& logPrefix, const std::string& logPath) {
    std::call_once(g_initLogger, [&] {
    g_logger = g3::LogWorker::createLogWorker();
    g_loggerHandle = g_logger->addDefaultLogger(logPrefix, logPath);
    g3::initializeLogging(g_logger.get());
    });
    }
    bool IsLoggerEnabled() {
    return g3::internal::isLoggingInitialized();
    }

    void ShutdownLogging() {
    //g3::internal::shutDownLogging(); // HERE I HAVE commented as I calls internally
    g_logger.reset();
    }

    I CALL ABOVE METHODS IN MY METHOD WHICH I CALL IT FROM MY GUI TOOL BUILT IN JAVA

    JNIEXPORT jobjectArray JNICALL Java_com_mydll_loadDLLGetVersion(JNIEnv *env, jobject thisObj)
    {
    int versionNumber = 0;
    string resultStr;
    jobjectArray jresultStr = (*env).NewObjectArray(3, (*env).FindClass(“java/lang/String”), NULL);

    try
    {
    // Tried initializing here too however it is crashing
    /*
    auto worker = g3::LogWorker::createLogWorker();
    auto handle = worker->addDefaultLogger(“MyG3Log_”, “D:\\G3Logs\\”);
    g3::initializeLogging(worker.get());
    */
    InitializeLogging(“MyG3Log_”, “D:\\G3Logs\\”);

    LOG(INFO) << "Hello1";
    LOG(INFO) << "Hello2";
    LOG(INFO) << "Hello3";
    LOG(INFO) << "Hello4";
    LOG(INFO) << "Hello5";
    LOG(INFO) << "Hello6";
    LOG(INFO) << "Hello7";

    ShutdownLogging();
    }
    }
    catch (exception& e)
    {
    if(handleDLL != NULL) {
    UnloadDLLAPI(handleDLL);
    }
    resultStr += ";";
    resultStr += e.what();
    }
    return jresultStr;
    }
    I GET BELOW ERROR FROM g3Log

    g3log g3FileSink shutdown at: 15:42:22
    Log file at: [D:/G3Logs/MyG3Log_20190307-154222.log]

    FATAL CALL but logger is NOT initialized
    CAUSE: EXCEPTION_ACCESS_VIOLATION
    Message:
    2019/03/07 15:42:56

    ***** FATAL EXCEPTION RECEIVED *******

    ***** Vectored Exception Handler: Received fatal exception EXCEPTION_ACCESS_VIOLATION PID: 13048

    ******* STACKDUMP *******
    stack dump [0]
    stack dump [1]
    stack dump [2]
    stack dump [3]
    stack dump [4]
    stack dump [5]
    stack dump [6]
    stack dump [7]
    stack dump [8]
    stack dump [9]
    stack dump [10]
    stack dump [11]
    stack dump [12]
    stack dump [13]
    stack dump [14]
    stack dump [15]

    PLEASE HELP ME WHERE I AM WRONG HERE

    • kjellkod says:

      1. Don’t post issues here. Post it in the repo.

      2. You never call the top I initialization only the bottom one. Once it goes out of scope the logger is deleted and get shutdown automatically.

      3. The way to stop the logger is through RAII not like this. Please read the docs in the repo.

      4. Your log output isn’t complete.

      Please review my reply and please use the repo g3log to communicate any issues you are having. This blog is not a bug tracking system

      Cheers
      Kjell

Leave a comment