C++20 ist nun bereits schon auf den Weg in die Compiler und es juckt mich in den Fingern die neuen Dinge direkt auszuprobieren. Allerdings soll diese lib nicht nur als Spielplaztz für Neues dienen, sondern soll auch in naher Zukunft produktiv von mir eingesetzt werden.
Derzeit sind leider noch nicht alle Features (vollends) implementiert (modules, source_location, ranges, etc.), weswegen C++20 natürlich nur bedingt zum Einsatz kommen kann. Ich werde daher von Zeit zu Zeit immer wieder Dinge austauschen und umbauen, wobei die generelle API erhalten bleiben soll.
Neben der intensiveren Einarbeitung in den neuen C++ Standard versuche ich dieses Projekt ebenfalls dazu zu nutzen, mich in CMake weiterzuentwickeln und etwas über automatisierungen über github workflows (und auch ganz Allgemein einen professionelleren Umgang mit Git) zu lernen. Es geht mir daher eher weniger darum mit irgendwelchen existierenden (oder noch kommenden) Libraries zu konkurieren oder alles "besser" machen zu wollen. Ich möchte experiementen, lernen und Erfahrungen gewinnen und im optimalfall ein solides Produkt abliefern. Daher wäre ich sehr dankbar über Jeden, der einfach mal über das Projekt schauen würde und Feedback da lässt.
Das Repo findet ihr hier:
https://github.com/DNKpp/Simple-Log
workflows
Jeder push ins github repo lässt nun 4 build&test actions starten: msvc (win), clang-cl (win), clang (ubuntu), gcc (ubuntu)
Zusätzlich triggered jeder push in den master branch einen doxygen worker, der mir aus den source files (oder viel eher headern) eine Dokumentation erstellt und diese direkt als github-page bereitstellt. Diese ist
hier zu finden.
CMake
Das komplette Projekt kann mit Hilfe von CMake gebaut und auch als library in anderen Projekten eingebaut werden (tatsächlich ist die Library an sich derzeit Header-Only. Lediglich Tests und Examples lassen sich wirklich bauen). Wie in der Readme zu lesen, muss lediglich das target "simple_log" über cmake gelinked werden. Gleichzeitig lässt sich das Bauen der Beispiele als Option steuern.
Pullen über FetchContent sollte daher auch ohne Probleme möglich sein (noch nicht getestet, ist aber ein Ziel).
Die Library selbst
Mir war es wichtig, dass es eine einfach zu nutzende API gibt, die jedoch von Nutzern anpassbar bleibt. Kern und Angelpunkt sind folgende drei Bereiche:
* Core
* Sinks
* Logger
Core
Dies ist praktisch der zentrale Bestandteil der library. An Cores können Sinks registriert werden, welche alle Nachrichten weitergeleitet bekommen, die über verbundene Logger instanzen generiert wurden. Es ist daher zwingend erforderlich, dass mindestens eine Core Instanz existert, welche ihre Logger auch "überlebt". Die Anzahl ist allerdings nicht auf eins limiert (kein Singleton, hurra!). Intern nutzt jeder Core seinen eigenen WorkerThread, welcher durch eine simple BlockingQueue Implementierung gefüttert wird.
Sinks
Das sind Objekte, mit deren Hilfe Nachrichten in Dateien gespeichert oder auf der Konsole ausgegeben werden können. Prinzipiell bietet das BasicSink ein Interface, dass sich mit jedem std::ostream benutzen lässt. Auf Sink Ebene lassen sich zusätzlich Filter installieren, mit deren Hilfe bestimmte Arten von Nachrichten ignoriert werden können. Ebenso lässt sich das Format, in welchem die Nachrichten an die streams übergeben werden sollen, anpassen.
Logger
Diese Objekte sind praktisch das Hauptinterface um eine Nachricht abzusetzen. Logger sind an sich leichtgewichtig, sind aber an eine explizite Core Instanz gebunden. Logger bieten die Möglichkeit default Severities oder Channel an eine Nachricht anzuhängen. Diese Default Werte lassen sich allerdings auch einfache Art und Weise temporär auf Nachrichten-Level überschreiben.. Eine Nachricht lässt sich recht simpel via << operatoren zusammenbauen.
|
C-/C++-Quelltext
|
1
|
log() << "Hallo," << " Welt" << "!"; // log stellt hier eine Logger Instanz dar. Der Operator () erstellt eine RecordBuilder Instanz, die über die << Ops weiter gereicht wird.
|
Record
Erwähnenswert ist an dieser Stelle noch das Record System. Ein Record ist hier nichts anderes als ein concept, dass das von der lib erwartete Interface und einige typedefs vorgibt. Die Library stellt mit BaseRecord eine eigene Implementierung zur Verfügung, die eben dieses concept erfüllt. Es ist allerdings nicht notwendig dieses auch zu nutzen. Das eröffnet Nutzern Möglichkeiten von einfachen Typenanpassungen der Member (anderer SeverityLevel typ oder Channel typ) bis hin zu kompletten custom Implementierungen mit vielen weiteren Membern, die fürs Logging benötigt werden.
Für mehr Infos, einfach mal hier nachschauen:
https://dnkpp.github.io/Simple-Log/
Mit diesen drei Puzzleteilchen ist es auch schon möglich die komplette logging Architektur zu nutzen.
Beispiel
Das hier wäre ein simples Beispiel um simples logging zu ermöglichen (mal ganz dreist das Basic Example kopiert
).
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
// This header contains preset types, which users can use to get an easy start with the library.
#include <Simple-Log/PresetTypes.hpp>
#include <iostream>
#include <memory>
/* let's use a namespace alias for convenience; be aware: if you use cmath, some stl implementations will bloat your global namespace with a log function declaration (c-relict).
Thus to make it compatible with all compilers, I'll use logging as alias instead.
All preset type alias are located in the sl::log::pre namespace, thus they do not interfere with the actual library if you don't want them to.
It is no good style just importing everything into your namespace. Just create an namespace alias like so. This way it's very easy to make it less verbose for you.
*/
namespace logging = sl::log::pre;
// We're using a factory function here, but this isn't necessary. You could also create a plain Core instance and set it up later in main
inline std::unique_ptr<logging::Core_t> makeLoggingCore()
{
/* an sl::log::Core is non-copy and non-movable, thus we will store it in an unique_ptr, so we
* can safely move it into our global.*/
auto core = std::make_unique<logging::Core_t>();
/* register a BasicSink and link it to the std::cout stream. This will simply print all incoming
messages onto the console.*/
core->makeSink<logging::BasicSink_t>(std::cout);
return core;
}
/* For conveniences we will simply store the core and our default logger as a global. Feel free to do it
otherwise. Just make sure the Core instance doesn't get destructed before all related Logger instances.*/
inline std::unique_ptr<logging::Core_t> gLoggingCore{ makeLoggingCore() };
inline logging::Logger_t gLog{ *gLoggingCore, logging::SeverityLevel::info };
int main()
{
gLog() << "Hello, World!"; // This will print this message with the "info" severity
// override default logger settings at the beginning
gLog() << logging::SetSev::debug << "Mighty debug message";
// or at the end
gLog() << "Print my important hint!" << logging::SetSev::hint;
// or in between of messages
gLog() << "This" << " will " << logging::SetSev::warning << " create " << " a " << " concatenated " << " warning " << " message";
// and using default setup again
gLog() << "Back to info";
}
/*Core will make sure, that all pending Records will be processed before it gets destructed.*/
/*
* The above code will for example generate this output:
* 18:49:32.047 >>> info:: Hello, World!
* 18:49:32.047 >>> debug:: Mighty debug message
* 18:49:32.047 >>> hint:: Print my important hint!
* 18:49:32.047 >>> warning:: This will create a concatenated warning message
* 18:49:32.047 >>> info:: Back to info
*
* Keep in mind, you are completely free how you are going to format your message. This is just the default one.
*/
|
Nachdem nun wahrscheinlich die Wenigsten hier bis zum Schluss lesen, würde ich mich dennoch über konstruktive Kritik oder Anmerkungen freuen.
Mit freundlichen Grüßen
Dominic