RafaGago / mini-async-log-c

Mini async log C port. Now with C++ wrappers.
BSD 3-Clause "New" or "Revised" License
70 stars 7 forks source link
c c11 cpp fast logger low-latency

Notice

I'm leaving Github. The main official location for this project is now: https://codeberg.org/RafaGago/mini-async-log-c

Description

A C11/C++11 low-latency wait-free producer (when using Thread Local Storage) asynchronous textual data logger with type-safe strings.

Based on the lessons learned on its older C++-only counterpart "mini-async-log".

AFAIK, this may be the fastest generic data logger for C or C++.

Features

Common:

C++ only:

Design

The life of a log message under this logger is.

At compile time

At run time

The log queue

It's the well known Dmitry Vyukov's intrusive MPSC queue. Wait-free producers. Blocking consumer. Perfect for this use case. Very good performance under low contention and good performance when contended.

The memory

3 configurable memory sources:

All three sources can be used together, separately or mixed. The priority order is: TLS, bounded, allocator.

Formatting

This is already hinted, there is no formatting on the producer-side. It happens on the consumer side and its cost is masked by file-io.

Usage Quickstart

Look at the examples folder.

https://github.com/RafaGago/mini-async-log-c/tree/master/example/src

Log format strings

The format strings have this (common) bracket delimited form:

"this is value 1: {} and this is value 2: {}"

The opening brace '{' is escaped by doubling it. The close brace doesn't need escaping.

Depending on the data type modifiers inside the brackets are accepted. Those try to match own printf's modifiers.

As a reminder, "printf" format strings are composed like this:

%[flags]\[width][.precision]\[length]specifier

Where the valid chars for each field are (on C99):

Malc autodetecs the datatypes passed to the log strings, so the "length" modifiers and signedness "specifiers" are never required. Which specifiers can be used on which data type is also restricted only to those that make sense.

Malc adds a non-numeric precision specifier: 'w'

'w' represents the maximum digit count excluding sign that the maximum value of a given type can have. It is aware if the type is going to be printed in decimal, hexadecimal or octal base.

On malc there are two precision modifier groups [0-9], w. Both of them are mutually exclusive. Only one of them can be specified, the result of not doing so is undefined.

Next comes a summary of the valid modifiers for each type.

integrals (or std::vector of integrals)

floating point (or std::vector of floating point)

others

Other types, like pointers, strings, etc. accept no modifiers.

examples

"this is a fixed 8 char width 0-padded integer: {08}"

"hex 8 char width zero padded integer: {08x}"

"0-padded integer to the digits that the datatype's max/min value has: {.w}"

"0-padded hex integer to the datatype's nibble count: {.wx}"

"Escaped open brace: {{"

Build and test

Linux

Meson is used.

debian/ubuntu example:

git submodule update --init --recursive

sudo apt install ninja-build python3-pip
sudo -H pip3 install meson

MYBUILDDIR=build
meson $MYBUILDDIR  --buildtype=release
ninja -C $MYBUILDDIR
ninja -C $MYBUILDDIR test

Windows

Acquire meson and ninja, if you are already using Python on Windows you may want to intall meson by using a python package manager (e.g. pip) and then install Ninja (ninja-build) separately.

If you don't, the easiest way to add all the dependecies is to download meson + Ninja as an MSI installer from meson's site:

https://mesonbuild.com/Getting-meson.html

Once you have meson installed the same steps as on Linux apply. Notice that:

Without build system (untested)

Compile every file under "src" and use "include" as include dir. This will still have challenges:

#ifndef __MALC_CONFIG_H__
#define __MALC_CONFIG_H__

/* autogenerated file, don't edit */

#define MALC_VERSION_MAJOR 1
#define MALC_VERSION_MINOR 0
#define MALC_VERSION_REV   0
#define MALC_VERSION_STR   "1.0.0"
#define MALC_BUILTIN_COMPRESSION 0
#define MALC_PTR_MSB_BYTES_CUT_COUNT 0

#endif /* __MALC_CONFIG_H__ */

Linking

This library links to a tiny own utility library that I use for C resources that aren't project specific, so I can reuse them.

Both "malc" and this library are meant to be statically linked to your project.

If you run


DESTDIR=$PWD/dummy-install ninja -C $MYBUILDDIR install

You may see these files on the library section:

└── lib
    └── x86_64-linux-gnu
        ├── libbl-base.a
        ├── libbl-getopt.a
        ├── libbl-nonblock.a
        ├── libbl-serial.a
        ├── libbl-taskqueue.a
        ├── libbl-time-extras.a
        ├── libcmocka.a
        ├── libmalc.a
        ├── libmalcpp.a
        └── pkgconfig
            ├── bl-base.pc
            ├── bl-getopt.pc
            ├── bl-nonblock.pc
            ├── bl-serial.pc
            ├── bl-taskqueue.pc
            ├── bl-time-extras.pc
            ├── malc.pc
            └── malcpp.pc

Your C project will need to link against: "libbl-time-extras.a", "libbl-base.a" "libbl-nonblock.a" and "libmalc.a". Your C++ project will need also to link "libmalcpp.a".

Some useful build parameters and compile time macros

Build parameters (meson)

See "meson_options.txt". These parameters end up on "include/malc/config.h".

Compile time macros

Gotchas

Crash handling

This library doesn't install any crash handlers because in my opionion it is out of the scope of a logging library.

If you want to terminate and flush the logger from a signal/exception handler, you can call "malc_terminate (ptr, false)" from there.

Lazy-evaluation of parameters

Both the C and the C++ log functions doesn't have function-like semantics:

This is deliberate, so you can place expensive function calls as log arguments.

Asynchronous logging

When timestamping at the producer thread is enabled (the default is platform dependant), there is the theoretical possibility that some entries show timestamps that go backwards in time some fractions of a second. This is expected. Consider this case:

A big mutex on the queue wouldn't theoretically show this behavior, but then:

Thread safety of values passed by reference to the logger

All values passed by pointer to malc are assumed to never be modified again by any other thread. The results of doing so are undefined.

Tradeoffs

When using this library remember that this is an asynchronous logger designed to optimize the caller/producer site. Consider if your application really needs an asynchronous logger and if performance at the call site is more important than the added complexity of having an asynchronous logger.

Tradeoffs: