Voor een hobbyprojectje ben ik een daemon/service aan het schrijven die tenminste onder UNIX en Windows moet gaan draaien. Omdat dit een achtergrondproces is, moet het programma af en toe wat informatie doorgeven over de operatie. Die informatie komt in verschillende categorien, zoals foutmeldingen, informatieve mededelingen en debug informatie. De laatste categorie mag in principe gewoon naar standard output, maar de eerste twee zijn nogal afhankelijk van het platform en de siutatie waarin ze draaien; bijvoorbeeld, foutmeldingen gaan bij het debuggen gewoon naar standard error, maar als Windows service naar de event logger en als UNIX daemon naar syslog.
Op dit moment schrijf ik alle berichten gewoon naar standard output met gebruik van std::cout. Dat werkt erg prettig, omdat ik dan allerlei standaardtypes als integer, doubles, strings, enzovoorts zonder gedoe kan weergeven. Nu wil ik echter een systeem ontwikkelen waarbij ik op deze gemakkelijke manier berichten kan formatteren, maar dat die wel op de juiste plek terecht komen.
Mijn eerste gedachte was om een aantal klassen te schrijven die van std::streambuf erven zodat ik gewoon naar een std::ostream object kan schrijven en dat de inhoud dan ergens anders heengaat (syslog, stdout, event log). Ik maak dan aparte std::ostream objecten voor verschillende doelen (bijvoorbeeld eentje voor foutmeldingen en eentje voor debug messages). Bij het opstarten initialiseer ik de references naar die objecten afhankelijk van de situatie.
Dit werkt best aardig, maar het eerste probleem is al dat het op deze manier nogal lastig is om aparte berichten te onderscheiden; bijvoorbeeld: wanneer zijn drie afzonderlijke regels die afzonderlijke berichten en wanneer is het een enkel bericht dat uit drie regels bestaat?
Om dit probleem te verhelpen dacht ik eraan om een stream modifier te schrijven die het einde van een bericht markeert (een soort std::endl maar dan anders). Maar als ik eenmaal begin met stream modifiers, zijn de mogelijkheden nog veel groter. Ik kan immers ook elk bericht vooraf laten gaan met een modifier die aangeeft wat voor soort melding het is. Dat heeft als voordeel dat ik niet een vast aantal streams hoef aan te maken, wat vooral handig is voor debug traces waarbij het handig kan zijn om een groot aantal debug traces te maken. In het gebruik zou het er dan ongeveer zo uit moeten komen te zien:
Op zich een mooie oplossing, dacht ik zo, behalve dat dit alleen in een singlethreaded omgeving werkt, en dat is dus het tweede probleem met deze aanpak. Een fix/hack hiervoor zou zijn om een lock te pakken bij het verwerken van zo'n stream modifier die het begin van een bericht aangeeft (zoals log::info in mijn voorbeeld) en die weer vrij te geven bij het verwerken van en log::eot modifier. Echt mooi vind ik dat echter niet.
Als alternatief dacht ik eraan om het threading probleem op te lossen door af te stappen van het idee van een centraal ostream-object waar naar geschreven wordt. Als elk bericht toch vooraf gegaan wordt door een modifier met een constructor, dan kan die modifier zelf ook wel als ostream dienstdoen:
Het idee is dat op deze manier lokaal een bericht wordt opgebouwd dat in een keer weggeschreven wordt. Tijdens het opbouwen van het bericht wordt de staat lokaal in de thread bijgehouden en dat is dus thread safe. Maar volgens mij is het allemaal ontzettend ingewikkeld om dat ueberhaupt werkend te krijgen omdat die temporary const moet zijn en 'ie waarschijnlijk al destructed is voor ik er voor de tweede keer naar kan schrijven. Ik heb nog niet in detail uitgezocht hoe dit er dan uit zou moeten komen te zien.
Al met al kom ik dus niet echt tot een bevredigende oplossing, vandaar dat ik hier om suggesties wil vragen. Meer concrete vragen zijn: hoe doen jullie multithread ostream-style logging in C++? Of, indien jullie dat helemaal niet doen omdat het veel handiger anders kan, hoe dan? Slaat mijn idee een beetje ergens op, vergis ik me hier en daar, of klopt het wel maar is het allemaal nodeloos onhandig? Graag jullie reacties.
Op dit moment schrijf ik alle berichten gewoon naar standard output met gebruik van std::cout. Dat werkt erg prettig, omdat ik dan allerlei standaardtypes als integer, doubles, strings, enzovoorts zonder gedoe kan weergeven. Nu wil ik echter een systeem ontwikkelen waarbij ik op deze gemakkelijke manier berichten kan formatteren, maar dat die wel op de juiste plek terecht komen.
Mijn eerste gedachte was om een aantal klassen te schrijven die van std::streambuf erven zodat ik gewoon naar een std::ostream object kan schrijven en dat de inhoud dan ergens anders heengaat (syslog, stdout, event log). Ik maak dan aparte std::ostream objecten voor verschillende doelen (bijvoorbeeld eentje voor foutmeldingen en eentje voor debug messages). Bij het opstarten initialiseer ik de references naar die objecten afhankelijk van de situatie.
Dit werkt best aardig, maar het eerste probleem is al dat het op deze manier nogal lastig is om aparte berichten te onderscheiden; bijvoorbeeld: wanneer zijn drie afzonderlijke regels die afzonderlijke berichten en wanneer is het een enkel bericht dat uit drie regels bestaat?
Om dit probleem te verhelpen dacht ik eraan om een stream modifier te schrijven die het einde van een bericht markeert (een soort std::endl maar dan anders). Maar als ik eenmaal begin met stream modifiers, zijn de mogelijkheden nog veel groter. Ik kan immers ook elk bericht vooraf laten gaan met een modifier die aangeeft wat voor soort melding het is. Dat heeft als voordeel dat ik niet een vast aantal streams hoef aan te maken, wat vooral handig is voor debug traces waarbij het handig kan zijn om een groot aantal debug traces te maken. In het gebruik zou het er dan ongeveer zo uit moeten komen te zien:
C++:
1
2
3
| log << log::info() << "Application started." << log::eot; log << log::trace(10) << "Incoming request received from " << remote_host << log::eot; log << log::trace(50) << "Random number generator initialized." << log::eot; |
Op zich een mooie oplossing, dacht ik zo, behalve dat dit alleen in een singlethreaded omgeving werkt, en dat is dus het tweede probleem met deze aanpak. Een fix/hack hiervoor zou zijn om een lock te pakken bij het verwerken van zo'n stream modifier die het begin van een bericht aangeeft (zoals log::info in mijn voorbeeld) en die weer vrij te geven bij het verwerken van en log::eot modifier. Echt mooi vind ik dat echter niet.
Als alternatief dacht ik eraan om het threading probleem op te lossen door af te stappen van het idee van een centraal ostream-object waar naar geschreven wordt. Als elk bericht toch vooraf gegaan wordt door een modifier met een constructor, dan kan die modifier zelf ook wel als ostream dienstdoen:
C++:
1
| log::trace(100) << "The aliens have landed at " << time << "!"; |
Het idee is dat op deze manier lokaal een bericht wordt opgebouwd dat in een keer weggeschreven wordt. Tijdens het opbouwen van het bericht wordt de staat lokaal in de thread bijgehouden en dat is dus thread safe. Maar volgens mij is het allemaal ontzettend ingewikkeld om dat ueberhaupt werkend te krijgen omdat die temporary const moet zijn en 'ie waarschijnlijk al destructed is voor ik er voor de tweede keer naar kan schrijven. Ik heb nog niet in detail uitgezocht hoe dit er dan uit zou moeten komen te zien.
Al met al kom ik dus niet echt tot een bevredigende oplossing, vandaar dat ik hier om suggesties wil vragen. Meer concrete vragen zijn: hoe doen jullie multithread ostream-style logging in C++? Of, indien jullie dat helemaal niet doen omdat het veel handiger anders kan, hoe dan? Slaat mijn idee een beetje ergens op, vergis ik me hier en daar, of klopt het wel maar is het allemaal nodeloos onhandig? Graag jullie reacties.