Logger Klasse
-
Hi,
ich möchte eine logger Klasse erstellen. Eine Instanz der Klasse soll sich im Wesentlichen wie ein ofstream verhalten, nur dass ich mich zwischen die Eingabe "von außen in die Instanz" und die Ausgabe in eine Datei klemmen möchte, um Zusatzinformationen anzubringen. Das soll jedes Mal geschehen, wenn eine neue Ausgabe nach "endl" gefordert wird. (Edit: Also am Anfang jeder neuen Zeile.)
Wie mache ich das jetzt am geschicktesten? Vermutlich sollte ich meine Klasse von streambuf/filebuf ableiten, aber mir will irgendwie nicht so recht klar werden, wie das Ganze funktionieren soll.
Hat jemand hier zufällig Ansätze/Anregungen/Links?
Ich kenne bereits:
http://www.c-plusplus.net/forum/152915
Allerdings ist mir da immer noch nicht ganz klar geworden, wie so etwas funktionieren könnte; falls ich da was übersehen habe einfach noch mal explizit drauf hinweisen.
-
Der Stream-Buffer wäre auch mein erster Ansatzpunkt - in dem Fall würde ich von einem normalen fstreambuf ableiten und die sputc() Methode überschreiben, um auf '\n' zu reagieren.
Ein anderer Ansatz wäre es, eine eigene Klasse (außerhalb der Stream-Hierarchie) zu bauen, die selber den Ausgabe-Operator überlädt und die Daten nach Bedarf nachbearbeitet (z.B. in einen Stringstream packen, Zeilenwechsel durch Log-Informationen ersetzen und anschließend weiterschicken).(btw, dir ist hoffentlich klar, daß man Zeilenwechsel auch direkt in den Stream schreiben kann, ohne dafür std::endl zu verwenden=
-
CStoll schrieb:
fstreambuf
fstreambuf ist mir nicht bekannt, meinst du streambuf? ( http://www.cplusplus.com/reference/iostream/streambuf/ ) // Meine Referenz
CStoll schrieb:
ableiten und die sputc() Methode überschreiben, um auf '\n' zu reagieren.
Hm.. ich habe damit leider wirklich noch wenig Erfahrung, ist garantiert, dass sputn auch nur sputc aufruft?
(Ein ganz kurzer Beispielcode an dem ich mich orientieren kann wäre im Übrigen klasse :))CStoll schrieb:
die selber den Ausgabe-Operator überlädt
<< Genau das versuche ich gerade zu vermeiden.
CStoll schrieb:
(btw, dir ist hoffentlich klar, daß man Zeilenwechsel auch direkt in den Stream schreiben kann, ohne dafür std::endl zu verwenden=
Grundsätzlich schon, hatte ich aber gar nicht bedacht. Macht das einen Unterschied für sputc
Falls ja, wäre es doch eigentlich nicht einmal schlecht, so quasi einen "manuellen" Zeilenumbruch zu ermöglichen.
-
cooky451 schrieb:
CStoll schrieb:
fstreambuf
fstreambuf ist mir nicht bekannt, meinst du streambuf? ( http://www.cplusplus.com/reference/iostream/streambuf/ ) // Meine Referenz
eigentlich meinte ich filebuf, aber vermutlich kann man auch direkt mit der Basisklasse arbeiten.
CStoll schrieb:
ableiten und die sputc() Methode überschreiben, um auf '\n' zu reagieren.
Hm.. ich habe damit leider wirklich noch wenig Erfahrung, ist garantiert, dass sputn auch nur sputc aufruft?
(Ein ganz kurzer Beispielcode an dem ich mich orientieren kann wäre im Übrigen klasse :))Bin ich mir jetzt nicht sicher - wobei du recht haben könntest, daß sputn() auch direkt ausgeben könnte. Da bliebe als Alternative noch die overflow()-Methode.
(wobei ich auch nicht sicher bin, wie der filebuf dort optimiert ist)Beispiele kann ich dir jetzt leider nicht bieten.
CStoll schrieb:
die selber den Ausgabe-Operator überlädt
<< Genau das versuche ich gerade zu vermeiden.
So schwer ist das doch auch nicht - kannst ja alles mit einem Template erledigen.
CStoll schrieb:
(btw, dir ist hoffentlich klar, daß man Zeilenwechsel auch direkt in den Stream schreiben kann, ohne dafür std::endl zu verwenden=
Grundsätzlich schon, hatte ich aber gar nicht bedacht. Macht das einen Unterschied für sputc
Falls ja, wäre es doch eigentlich nicht einmal schlecht, so quasi einen "manuellen" Zeilenumbruch zu ermöglichen.Für den Stream-Buffer macht es keinen Unterschied, ob du '\n' oder endl rausgegeben hast - den Unterschied behandelt der Stream selber.
(bzw. der endl-Manipulator macht auch nichts anderes als '\n' zu schreiben und anschließend flush() aufzurufen)
-
So. Unter Zuhilfenahme von
http://www.c-plusplus.net/forum/152915
http://www.c-plusplus.net/forum/288386
http://www.c-plusplus.net/forum/p1008097#1008097habe ich es dann geschafft, mir zumindest eine Klasse (bzw. 2) zu basteln die das gewünschte mehr oder weniger umsetzt. Allerdings scheint mir so ein Konstrukt gerade fast schon eleganter:
EDIT:
Sollte alles gelöst sein, ich poste die Klasse nachher mal, vielleicht interessiert das ja irgendjemanden.
-
Wäre noch offen für Kritik, funktioniert aber ganz gut soweit.
Visual Studio will mir noch irgendetwas zu dem Template sagen, aber es ist weder eine Warnung noch ein Fehler. Vielleicht wisst ihr ja mehr.
Edit:
Argh, mir fällt gerade auf, dass so natürlich jeder Manipulator als std::endl erkannt wird. Wie kann ich std::endl jetzt möglichst sicher von den anderen Trennen?Edit:
Siehe Code unten.
-
class CLogger { // ... CLogger(std::string& filename) { CLogger(filename.c_str()); }
Das geht so nicht. Bzw. es geht schon, aber es macht nicht was du denkst. (Der Aufruf von
CLogger(filename.c_str())
erstellt hier ein temporäres Objekt.)Was den Rest angeht würde ich vorschlagen das in zumindest zwei Klassen zu trennen: eine "LogMessage" Klasse und eine "Logger" Klasse.
Die "LogMessage" speichert alle Infos zu einer Log-Ausgabe. D.h. die Zeit zu der die "LogMessage" erzeugt wurde, ggf. den Filenamen + Zeilenummer wo im Programm die Logausgabe steht, die "severity" etc. Und natürlich den eigentlichen Inhalt (den "Text").
Die "LogMessage" kümmert sich also um das "Was" und das "Woher".Die "Logger" Klasse kümmert sich um das Schreiben der Message in ... naja, wo auch immer hin. In ein File, in den Debug-Stream etc. Weiters kümmert sich die "Logger" Klasse um die Formatierung, das voranstellen der Header (Zeit, Thread-ID - was man halt haben möchte) etc.
Der "Logger" kümmert sich also um das "Wie" und das "Wohin".Natürlich kann man es noch weiter auftrennen, aber dann wird es dementsprechend komplizierter.
-
hustbaer schrieb:
Die "LogMessage" kümmert sich also um das "Was" und das "Woher".
[...]
Die "LogMessageger" kümmert sich also um das "Wie" und das "Wohin".
-
hustbaer schrieb:
Das geht so nicht. Bzw. es geht schon, aber es macht nicht was du denkst. (Der Aufruf von
CLogger(filename.c_str())
erstellt hier ein temporäres Objekt.)Wäre syntaxmäßig ja auch Quatsch, danke für den Hinweis. Was wäre eine gute Alternative, muss ich die Initialisierungsliste dann für jeden Konstruktor einzeln schreiben?
(Ein privates .init() wäre natürlich möglich, aber ist irgendwie auch nicht das Wahre.)hustbaer schrieb:
Was den Rest angeht würde ich vorschlagen das in zumindest zwei Klassen zu trennen: eine "LogMessage" Klasse und eine "Logger" Klasse.
Ich kann beim besten Willen keinen Vorteil darin sehen. Könntest du den Sinn der Aktion vielleicht weiter erläutern?
---
@Topic:
Weiter zu lösendes Problem:
Wie kann ich "std::ostream& (*f)(std::ostream&)" auf == &std::endl<char, std::char_traits<char>> testen?Habe das jetzt mal so gemacht, na ob das hübsch ist
class CLogger { public: CLogger(const char* filename = "log.log") : m_logfile(filename, std::ofstream::app), m_nextline(true) { *this << "Begin log." << std::endl; } CLogger(std::string& filename) : m_logfile(filename.c_str(), std::ofstream::app), m_nextline(true) { *this << "Begin log." << std::endl; } ~CLogger() { *this << "End log.\n\n"; } template<typename T> CLogger& operator << (const T& p) { if (!m_logfile.fail()) { if (m_nextline) { std::time_t t = std::time(0); std::string s = std::ctime(&t); *(s.end() - 1) = ':'; // switch '\n' to ':' m_logfile << s + ' '; m_nextline = false; } m_logfile << p; } return *this; } CLogger& operator << (std::ostream& (*f)(std::ostream&)) { if (!m_logfile.fail()) { if (f == static_cast<std::ostream& (*)(std::ostream&)>(&std::endl)) { m_nextline = true; } m_logfile << f; } return *this; } bool fail() const { return m_logfile.fail(); } private: std::ofstream m_logfile; bool m_nextline; };
-
Bei dem Klassennamen muesste fail() immer true liefern;)
-
this->that schrieb:
Bei dem Klassennamen muesste fail() immer true liefern;)
Ja stimmt, habe das jetzt angepasst.
(Also fail() liefert jetzt immer true.)
-
@brotbernd:
Danke, ist korrigiert.
-
cooky451 schrieb:
Ich kann beim besten Willen keinen Vorteil darin sehen. Könntest du den Sinn der Aktion vielleicht weiter erläutern?
Wenn du Logausgaben aus mehreren Threads machen willst, und nicht willst dass zwei gleichzeitige Ausgaben sich "vermischen", musst du irgendwie synchronisieren.
Den Logger zu "locken", dann Log-Message mit mehreren Funktionsaufrufen ausgeben, und danach den Logger wieder "unlocken" halte ich für unschön.
Schöner ist es zuerst die Log-Message zusammenzubauen, und sie dann mit einem einzigen Funktionsaufruf an den Logger zu übergeben.
Dazu brauchst du schonmal eine Klasse zum Log-Message zusammenbauen.Dann ist es damit auch einfach die selbe Log-Message in mehrere Logger zu schreiben, z.B. einen der in das Logfile schreibt, und einen der in den Debug-Output schreibt. Oder auf die Konsole.
Auch das Anhängen/Voranstellen einer Header (Zeit, Thread-ID, was auch immer) wird dadurch viel einfacher, weil die Message-Grenzen auf einmal klar definiert sind. Auch "fehlende" Newlines kann man einfach nach jeder Log-Message anhängen. Oder Log-Messages in eine Datenbank schreiben.
Und wenn du das System mal erweitern willst, kannst du jederzeit der Log-Message Klasse weitere Attribute verpassen. z.B. "wo" sie herkommt. Nicht nur Filename + Zeilennummer im Code, sondern evtl. auch ein Kürzel für das "Subsystem" das die Message generiert hat. Einen "Severity-Level" (z.B. Verbose-Info, Info, Warning, Error, Fatal-Error) etc.
Das alles können die Logger auswerten, um zu entscheiden ob die Message überhaupt geschrieben wird, oder wie sie formatiert werden soll etc.Es ist einfach wesentlich flexibler.