Design eines Loggers



  • Hallo Leute,

    Ich habe schon seit einigen Wochen das Problem, dass ich nicht genau weiß wie ich einen Logger in C++ designen soll. Ich habe schon viele Ansätze gesehen und irgendwie hat mich alles eher verwirrt, als dass ich sicherer wurde. Aus älteren Projekten kenne ich sowas wie LOG_INFO() oder LOG_ERROR(). Einfache Makros, die einen std::string als Parameter erwarten. Problem war allerdeings, dass man verschiedene Datentypen vorab manuell zusammenfassen musste.

    Beispiel:

    // Methode 1:
    LOG_INFO("Das ist eine Meldung");
    
    // Methode 2:
    std::stringstream ss;
    ss << "die Variable X mit den Wert " << myInt << " ist ungueltig";
    LOG_INFO(ss.str());
    

    Nun, das finde ich irgendwie nicht sehr elegant. Mir schwebt eher so etwas vor:

    // Variante A
    Log(INFO, "die Variable X mit den Wert " << myInt << " ist ungueltig");
    
    // Variante B
    Log(INFO) << "die Variable X mit den Wert " << myInt << " ist ungueltig";
    
    // Variante C
    LOG_INFO << "die Variable X mit den Wert " << myInt << " ist ungueltig";
    

    Variante A wäre schonmal interessant, allerdings glaube ich nicht dass man das so umsetzen kann. Variante B ist auch gut und soweit ich weiß umsetzbar. Im letzten Thread (https://www.c-plusplus.net/forum/337297) hat mir Finnegan den Tipp gegeben. Soweit ich gelesen habe, muss man den << Opterator überladen, aber ich habe es noch nicht hinbekommen. Variante C wäre mit Makro und würde noch die Möglichkeit bieten, __FILE__ und __LINE__ zu nutzen.

    Was bis jetzt funktionieren würde, wäre folgendes:

    class myLogger
    {
    public:
        myLogger() {}
        ~myLogger() {}
    
        void logInfo(std::string msg)
        {
            // hier kann ich 'msg' verarbeiten
        }
    };
    
    myLogger Log;
    
    int main()
    {
        Log.logInfo("Das ist eine Meldung");
    }
    

    Diese Variante ist nicht so toll und daher würde ich es wohl eher so machen:

    class myLogger
    {
    public:
        myLogger() {}
        ~myLogger() {}
    
        myLogger(int logLevel, std::string msg)
        {
        	//hier kann ich 'msg' verarbeiten
        }
    };
    
    int main()
    {
        myLogger(INFO, "Das ist eine Meldung");
    }
    

    'INFO' wird mal ein enum sein, mit verschiedenen Log-Leveln (debug, info, warn, error, fatal). Die Klasse ist hier auch nur minimal und wird später statische Variablen haben, in den z.B. festgelegt wird, wo die Logdaten gespeichert werden. Wie müsste ich hier den << Operator überladen, damit ich eine Funktion wie in Variante B hätte (myLogger(INFO) << "mein text"; )? Muss ich das global definieren oder in der Klasse? Wahrscheinlich müsste ich auch mit stringstream arbeiten anstatt mit string, oder?

    Was mir auch Kopfzerbrechen bereitet, ist die Frage wie stark die Anwendung belastet wird, wenn man viel loggt. ...solle man einen Logger gleich thread-sicher programmieren? ...sollte man Vorkehrungen treffen, damit der Logger nur einmal instanziiert werden kann? (stoße in Foren häufig auf den Begriff 'singleton').

    ...viele Fragen und maximale Verwirrung 😞 😕

    viele Grüße,
    SBond



  • Hi,
    die Anforderungen an's Logging sind je nach Applikation schon mal extrem unterschiedlich.

    Die Variante A hat den Vorteil, dass die einen Ausdruck übergeben kannst, der im Fall, dass der Log-Level nicht hoch genug ist, erst gar nicht ausgewertet wird.

    Vor ca. 100 Jahren hatte ich das hier mal geschrieben: http://robitzki.de/log.h

    Damit sind Ausdrücke wie:

    LOG_INFO("die Variable X mit den Wert " << myInt << " ist ungültig");
    

    möglich. Guck mal, ob Du damit was anfangen kannst oder ein paar Anregungen findest. Den Code darfst Du gerne nutzen (rest wäre in log.cpp und log_test.cpp

    mfg Torsten



  • cool, danke 🙂

    ich schaue es mir am Montag mal genauer an.

    noch frohe Ostern 😃



  • SBond schrieb:

    // Variante B
    Log(INFO) << "die Variable X mit den Wert " << myInt << " ist ungueltig";

    Das ist die Variante die ich im Moment verwende. Und auch diese lässt sich als Makro umsetzen.
    Mit diversen Kunstgriffen im "Log" Makro kann man es sogar hinbekommen dass der "<< x << y << z" Teil nur ausgewertet wird wenn der Logger überhaupt aktiv ist. Das ist günstig wenn man an performancekritischen Stellen logging einbauen möchte, welches im Normalbetrieb allein durch die Auswertung der Argumente für die << Operatoren schon bremsen würde -- selbst wenn der << Operator dann sieht dass es nichts zu tun gibt und entsprechend sofort "return" macht.

    Und dadurch dass es sich um ein Makro handelt kannst du auch in dieser Variante __FILE__, __FUNCTION__ und __LINE__ mitgeben.

    Wie man das machen kann kannst du dir z.B. bei Boost.Log abgucken.



  • Kann man Variante B überhaupt threadsafe implementieren? Also das diese Zeile garantiert als Ganzes geloggt wird.



  • Sicherlich.
    Du <<st ja nicht nach cout sondern in ein Log-Message Objekt. Das dann zum Schluss als ganzes .submit()ed wird.



  • Aber wie stellt man den Schluss fest? Also den letzten Aufruf von operator<<



  • hustbaer schrieb:

    SBond schrieb:

    // Variante B
    Log(INFO) << "die Variable X mit den Wert " << myInt << " ist ungueltig";

    Das ist die Variante die ich im Moment verwende.

    dito

    hmmmmmm schrieb:

    Aber wie stellt man den Schluss fest? Also den letzten Aufruf von operator<<

    Im Destruktor des Proxys.



  • hmmmmmm schrieb:

    Aber wie stellt man den Schluss fest? Also den letzten Aufruf von operator<<

    mit ner for-Schleife

    #define Log(channel) \
        for (LogMessage message(channel); message.IsActive(); message.Submit()) \
            message.GetStream() // - hier wird dann Zeugs reingeshiftet
    

    message.IsActive() muss dabei true zurückliefern wenn die Message noch nicht submitted wurde und der Log-Channel aktiv ist.



  • auf jeden Fall vielen Dank für die Antworten. Ihr habt mir sehr geholfen 🙂



  • hustbaer schrieb:

    hmmmmmm schrieb:

    Aber wie stellt man den Schluss fest? Also den letzten Aufruf von operator<<

    mit ner for-Schleife

    #define Log(channel) \
        for (LogMessage message(channel); message.IsActive(); message.Submit()) \
            message.GetStream() // - hier wird dann Zeugs reingeshiftet
    

    message.IsActive() muss dabei true zurückliefern wenn die Message noch nicht submitted wurde und der Log-Channel aktiv ist.

    Durch was wird die Schleife abgebrochen? Anderer Thread, der den Stream ausliest?



  • Ich vermute, die Schleife wird entweder genau einmal, oder niemals ausgeführt. Anders ergibt es keinen Sinn. Man hätte auch if schreiben können, dann müsste man aber einen umschließenden Scope haben, damit die Variable message nicht sichtbar ist. Sehr pfiffig 😉



  • volkard schrieb:

    Durch was wird die Schleife abgebrochen? Anderer Thread, der den Stream ausliest?

    Dadurch dass Submit aufgerufen wird.

    hustbaer schrieb:

    message.IsActive() muss dabei true zurückliefern wenn die Message noch nicht submitted wurde und der Log-Channel aktiv ist.



  • Torsten Robitzki schrieb:

    Ich vermute, die Schleife wird entweder genau einmal, oder niemals ausgeführt.

    Genau.
    0x wenn der Channel inaktiv ist und 1x wenn er aktiv ist.

    Torsten Robitzki schrieb:

    Anders ergibt es keinen Sinn.

    Genau.

    Torsten Robitzki schrieb:

    Man hätte auch if schreiben können, dann müsste man aber einen umschließenden Scope haben, damit die Variable message nicht sichtbar ist.

    Naja, message ist ja in der for-Lösung auch sichtbar. Daher verwendet man in echt dann auch nicht message als Bezeichner sondern eher sowas wie _message_NX2KLQA7MXHE21YLAW5DSVPQN0 . Um die Wahrscheinlichkeit klein zu halten dass der User-Programmer den Bezeichner in seinem Programm verwenden will.
    Die for-Schleife verwendet man deswegen, weil man dadurch den Submit Aufruf im "i++" Teil unterbringen kann. Das ginge bei if nicht.

    Torsten Robitzki schrieb:

    Sehr pfiffig 😉

    Ja. Eiskalt von Boost.Log nachgemacht.
    (EDIT: Weil unklar formuliert: Ich hab das natürlich bei Boost.Log abgeguckt - die hatten das vorher 😃 /EDIT)



  • Nu muss ich meinen Proxy löschen, deine Variante ist hübscher.
    😋



  • Naja dafür kannst du ne neue, schönere, bessere Klasse programmieren 😃

    Aber im Ernst, viel ändert sich dadurch ja nicht.

    Der vermutlich grösste Vorteil "meiner" Variante ist mMn. dass man unfertig befüllte Log-Messages erkennen kann. Also wenn beim "reinshiften" eine Exception fliegt. Lässt sich ja schön im Dtor der LogMessage * checken - quasi " if (!submitted) ThereWasAProblem(); ".
    Dann kann man z.B. ne spezielle Meldung rausloggen - evtl. mit dem unvollständigen Message-Text. Oder das Programm abbrechen. Bzw. vermutlich auf jeden Fall mal assert sagen, weil Exceptions beim Zusammenbasteln ner Log-Ausgabe sind ja normalerweise sehr unerwünscht.

    *: Name verbesserungswürdig - macht ja mehr als nur "message sein"


Log in to reply