LCOV - code coverage report
Current view: top level - libMagAOX/logger - logManager.hpp (source / functions) Coverage Total Hit
Test: MagAOX Lines: 73.0 % 122 89
Test Date: 2026-01-03 21:03:39 Functions: 76.5 % 17 13

            Line data    Source code
       1              : /** \file logManager.hpp
       2              :   * \brief The MagAO-X log manager.
       3              :   * \author Jared R. Males (jaredmales@gmail.com)
       4              :   *
       5              :   * \ingroup logger_files
       6              :   *
       7              :   * History:
       8              :   * - 2017-06-27 created by JRM
       9              :   */
      10              : 
      11              : #ifndef logger_logManager_hpp
      12              : #define logger_logManager_hpp
      13              : 
      14              : #include <memory>
      15              : #include <list>
      16              : 
      17              : #include <thread>
      18              : 
      19              : #include <mutex>
      20              : #include <ratio>
      21              : 
      22              : #include <mx/app/appConfigurator.hpp>
      23              : 
      24              : #include <flatlogs/flatlogs.hpp>
      25              : 
      26              : using namespace flatlogs;
      27              : 
      28              : #include "../common/defaults.hpp"
      29              : 
      30              : #include "generated/logTypes.hpp"
      31              : #include "generated/logStdFormat.hpp"
      32              : 
      33              : namespace MagAOX
      34              : {
      35              : namespace logger
      36              : {
      37              : 
      38              : /// The standard MagAOX log manager, used for both process logs and telemetry streams.
      39              : /** Manages the formatting and queueing of the log entries.
      40              :   *
      41              :   * A log entry is made using one of the standard log types.  These are formatted into a binary stream and
      42              :   * stored in a std::list.  This occurs in the calling thread.  The insertion into the list is mutex-ed, so
      43              :   * it is safe to make logs from different threads concurrently.
      44              :   *
      45              :   * Write-to-disk occurs in a separate thread, which
      46              :   * is normally set to the lowest priority so as not to interfere with higher-priority tasks.  The
      47              :   * log thread cycles through pending log entries in the list, dispatching them to the logFile.
      48              :   *
      49              :   * The template parameter logFileT is one of the logFile types, which is used to actually write to disk.
      50              :   *
      51              :   * Example:
      52              :     \code
      53              :     logManager<logFileRaw> log;
      54              :     log.m_logThreadStart();
      55              : 
      56              :     //do stuff
      57              : 
      58              :     log<loop_closed>(); //An empty log, which is used to log discrete events.
      59              : 
      60              :     //more stuff happens
      61              : 
      62              :     log<text_log>("a log entry with a std::string message type");
      63              : 
      64              :     \endcode
      65              :   *
      66              :   * \todo document all the requirements of logFileT
      67              :   *
      68              :   * \tparam logFileT a logFile type with a writeLog method.
      69              :   *
      70              :   * \ingroup logger
      71              :   */
      72              : template<class _parentT, class _logFileT>
      73              : struct logManager : public _logFileT
      74              : {
      75              :    typedef _parentT parentT;
      76              :    typedef _logFileT logFileT;
      77              : 
      78              : protected:
      79              :    parentT * m_parent {nullptr};
      80              : 
      81              :    ///\todo Make these protected members, with appropriate access methods
      82              :    //-->
      83              : public:
      84              :    std::string m_configSection {"logger"}; ///<The configuration files section name.  Default is `logger`.
      85              : 
      86              : protected:
      87              :    std::list<bufferPtrT> m_logQueue; ///< Log entries are stored here, and writen to the file by the log thread.
      88              : 
      89              :    std::thread m_logThread; ///< A separate thread for actually writing to the file.
      90              :    std::mutex m_qMutex; ///< Mutex for accessing the m_logQueue.
      91              : 
      92              :    bool m_logShutdown {false}; ///< Flag to signal the log thread to shutdown.
      93              : 
      94              :    unsigned long m_writePause {MAGAOX_default_writePause}; ///< Time, in nanoseconds, to pause between successive batch writes to the file. Default is 1e9. Configure with logger.writePause.
      95              : 
      96              : public:
      97              :    logPrioT m_logLevel {logPrio::LOG_INFO}; ///< The minimum log level to actually record.  Logs with level below this are rejected. Default is INFO. Configure with logger.logLevel.
      98              : 
      99              : protected:
     100              :    int m_logThreadPrio {0};
     101              : 
     102              :    bool m_logThreadRunning {false};
     103              :    //<--end of todo
     104              : 
     105              : public:
     106              :    /// Default c'tor.
     107              :    logManager();
     108              : 
     109              :    /// Destructor.
     110              :    ~logManager();
     111              : 
     112              :    /// Set the logger parent
     113              :    /** The parent is used for interactive presentation of log messages
     114              :      */
     115              :    void parent( parentT * p /**< [in] pointer to the parent object*/);
     116              : 
     117              :    /// Get the logger parent
     118              :    /**
     119              :      * Returns a point to the parent object.
     120              :      */
     121              :    parentT * parent();
     122              : 
     123              :    /// Set a new value of logShutdown
     124              :    /** Updates m_logShutdown with new value.
     125              :      *
     126              :      * \returns 0 on success
     127              :      * \returns -1 on error (if wp == 0).
     128              :      */
     129              :    int logShutdown( bool ls );
     130              : 
     131              :    /// Get the current value of logShutdown
     132              :    /** \returns the value m_logShutdown.
     133              :      */
     134              :    bool logShutdown();
     135              : 
     136              :    /// Set a new value of writePause
     137              :    /** Updates m_writePause with new value.
     138              :      *
     139              :      * \returns 0 on success
     140              :      * \returns -1 on error (if wp == 0).
     141              :      */
     142              :    int writePause( const unsigned long & wp /**< [in] the new value of writePause */);
     143              : 
     144              :    /// Get the current value of writePause
     145              :    /** \returns the value m_writePause.
     146              :      */
     147              :    unsigned long writePause();
     148              : 
     149              :    /// Set a new value of logLevel
     150              :    /** Updates m_logLevel with new value.
     151              :      * Will return an error and take no actions if the argument
     152              :      * is outside the range of the logLevels enum.
     153              :      *
     154              :      * \returns 0 on success
     155              :      * \returns -1 on error.
     156              :      */
     157              :    int logLevel( logPrioT newLev /**< [in] the new value of logLevel */);
     158              : 
     159              :    /// Get the current value of logLevel
     160              :    /** \returns the value m_logLevel
     161              :      */
     162              :    logPrioT logLevel();
     163              : 
     164              :    /// Set a new value of logThreadPrio
     165              :    /** Updates m_logThreadPrio with new value.
     166              :      * If the argument is < 0, this sets m_logThreadPrio to 0.
     167              :      * Will not set > 98, and returns -1 with no changes in this case.
     168              :      *
     169              :      * \returns 0 on success
     170              :      * \returns -1 on error (> 98).
     171              :      */
     172              :    int logThreadPrio( int newPrio /**< [in] the new value of logThreadPrio */);
     173              : 
     174              :    /// Get the current value of logThreadPrio
     175              :    /** \returns the value m_logThreadPrio
     176              :      */
     177              :    int logThreadPrio();
     178              : 
     179              :    /// Get status of the log thread running flag
     180              :    /** \returns the current value of m_logThreadRunning
     181              :      */
     182              :    bool logThreadRunning();
     183              : 
     184              :    ///Setup an application configurator for the logger section
     185              :    int setupConfig( mx::app::appConfigurator & config /**< [in] an application configuration to setup */);
     186              : 
     187              :    ///Load the logger section from an application configurator
     188              :    /**
     189              :      */
     190              :    int loadConfig( mx::app::appConfigurator & config /**< [in] an application configuration from which to load values */);
     191              : 
     192              : 
     193              :    ///Thread starter, called by logThreadStart on thread construction.  Calls logThreadExec.
     194              :    static void _logThreadStart( logManager * l /**< [in] a pointer to a logger instance (normally this) */);
     195              : 
     196              :    /// Start the logger thread.
     197              :    int logThreadStart();
     198              : 
     199              :    /// Execute the logger thread.
     200              :    void logThreadExec();
     201              : 
     202              :    /// Create a log formatted log entry, filling in a buffer.
     203              :    /** This is where the timestamp of the log entry is set.
     204              :      *
     205              :      * \tparam logT is a log entry type
     206              :      *
     207              :      * \returns 0 on success, -1 on error.
     208              :      */
     209              :    template<typename logT>
     210              :    static int createLog( bufferPtrT & logBuffer, ///< [out] a shared_ptr\<logBuffer\>, which will be allocated and populated with the log entry
     211              :                          const typename logT::messageT & msg, ///< [in] the message to log (could be of type emptyMessage)
     212              :                          const logPrioT & level  ///< [in] the level (verbosity) of this log
     213              :                        );
     214              : 
     215              :    /// Create a log formatted log entry, filling in a buffer.
     216              :    /** This version has the timestamp provided.
     217              :      *
     218              :      * \tparam logT is a log entry type
     219              :      *
     220              :      * \returns 0 on success, -1 on error.
     221              :      */
     222              :    template<typename logT>
     223              :    static int createLog( bufferPtrT & logBuffer, ///< [out] a shared_ptr\<logBuffer\>, which will be allocated and populated with the log entry
     224              :                          const timespecX & ts, ///< [in] the timestamp of this log entry.
     225              :                          const typename logT::messageT & msg, ///< [in] the message to log (could be of type emptyMessage)
     226              :                          const logPrioT & level ///< [in] the level (verbosity) of this log
     227              :                        );
     228              : 
     229              :    /// Make a log entry, including a message.
     230              :    /**
     231              :      * \tparam logT is a log entry type
     232              :      */
     233              :    template<typename logT>
     234              :    void log( const typename logT::messageT & msg, ///< [in] the message to log
     235              :              logPrioT level = logPrio::LOG_DEFAULT ///< [in] [optional] the log level.  The default is used if not specified.
     236              :            );
     237              : 
     238              :    /// Make a log entry, including a message.
     239              :    /**
     240              :      * \tparam logT is a log entry type
     241              :      */
     242              :    template<typename logT>
     243              :    void log( timespecX & ts, ///< [in] the timestamp of the log entry
     244              :              const typename logT::messageT & msg, ///< [in] the message to log
     245              :              logPrioT level = logPrio::LOG_DEFAULT ///< [in] [optional] the log level.  The default is used if not specified.
     246              :            );
     247              : 
     248              :    /// Make a log entry with no message.
     249              :    /**
     250              :      * \tparam logT is a log entry type
     251              :      */
     252              :    template<typename logT>
     253              :    void log( logPrioT level = logPrio::LOG_DEFAULT /**< [in] [optional] the log level.  The default is used if not specified.*/);
     254              : 
     255              :    /// Make a log entry with no message.
     256              :    /**
     257              :      * \tparam logT is a log entry type
     258              :      */
     259              :    template<typename logT>
     260              :    void log( timespecX & ts, ///< [in] the timestamp of the log entry
     261              :              logPrioT level = logPrio::LOG_DEFAULT ///< [in] [optional] the log level.  The default is used if not specified.
     262              :            );
     263              : 
     264              : };
     265              : 
     266              : template<class parentT, class logFileT>
     267         1194 : logManager<parentT, logFileT>::logManager()
     268              : {
     269          398 : }
     270              : 
     271              : template<class parentT, class logFileT>
     272          398 : logManager<parentT, logFileT>::~logManager()
     273              : {
     274          398 :    m_logShutdown = true;
     275              : 
     276          398 :    if(m_logThread.joinable()) m_logThread.join();
     277              : 
     278              :    //One last check to see if there are any unwritten logs.
     279          398 :    if( !m_logQueue.empty() ) logThreadExec();
     280              : 
     281          398 : }
     282              : 
     283              : template<class parentT, class logFileT>
     284          890 : void logManager<parentT, logFileT>::parent( parentT * p )
     285              : {
     286          890 :    m_parent = p;
     287          890 : }
     288              : 
     289              : template<class parentT, class logFileT>
     290              : parentT * logManager<parentT, logFileT>::parent()
     291              : {
     292              :    return m_parent;
     293              : }
     294              : 
     295              : template<class parentT, class logFileT>
     296            3 : int logManager<parentT, logFileT>::logShutdown( bool ls )
     297              : {
     298            3 :     m_logShutdown = ls;
     299            3 :     return 0;
     300              : }
     301              : 
     302              : template<class parentT, class logFileT>
     303              : bool logManager<parentT, logFileT>::logShutdown()
     304              : {
     305              :     return m_logShutdown;
     306              : }
     307              : 
     308              : template<class parentT, class logFileT>
     309              : int logManager<parentT, logFileT>::writePause( const unsigned long & wp )
     310              : {
     311              :    if(wp == 0) return -1;
     312              : 
     313              :    m_writePause = wp;
     314              : 
     315              :    return 0;
     316              : }
     317              : 
     318              : template<class parentT, class logFileT>
     319              : unsigned long logManager<parentT, logFileT>::writePause()
     320              : {
     321              :    return m_writePause;
     322              : }
     323              : 
     324              : template<class parentT, class logFileT>
     325            0 : int logManager<parentT, logFileT>::logLevel( logPrioT newLev )
     326              : {
     327              : 
     328              : 
     329            0 :    m_logLevel = newLev;
     330              : 
     331            0 :    return 0;
     332              : }
     333              : 
     334              : template<class parentT, class logFileT>
     335            0 : logPrioT logManager<parentT, logFileT>::logLevel()
     336              : {
     337            0 :    return m_logLevel;
     338              : }
     339              : 
     340              : template<class parentT, class logFileT>
     341              : int logManager<parentT, logFileT>::logThreadPrio( int newPrio )
     342              : {
     343              :    if(newPrio > 98) return -1; //clamped at 98 for safety.
     344              :    if(newPrio < 0) newPrio = 0;
     345              : 
     346              :    m_logThreadPrio = newPrio;
     347              :    return 0;
     348              : }
     349              : 
     350              : template<class parentT, class logFileT>
     351              : int logManager<parentT, logFileT>::logThreadPrio()
     352              : {
     353              :    return m_logThreadPrio;
     354              : }
     355              : 
     356              : template<class parentT, class logFileT>
     357           76 : bool logManager<parentT, logFileT>::logThreadRunning()
     358              : {
     359           76 :    return m_logThreadRunning;
     360              : }
     361              : 
     362              : template<class parentT, class logFileT>
     363           24 : int logManager<parentT, logFileT>::setupConfig( mx::app::appConfigurator & config )
     364              : {
     365          240 :    config.add(m_configSection+".logDir","L", "logDir",mx::app::argType::Required, m_configSection, "logDir", false, "string", "The directory for log files");
     366          240 :    config.add(m_configSection+".logExt","", "logExt",mx::app::argType::Required, m_configSection, "logExt", false, "string", "The extension for log files");
     367          240 :    config.add(m_configSection+".maxLogSize","", "maxLogSize",mx::app::argType::Required, m_configSection, "maxLogSize", false, "string", "The maximum size of log files");
     368          240 :    config.add(m_configSection+".writePause","", "writePause",mx::app::argType::Required, m_configSection, "writePause", false, "unsigned long", "The log thread pause time in ns");
     369          240 :    config.add(m_configSection+".logThreadPrio", "", "logThreadPrio", mx::app::argType::Required, m_configSection, "logThreadPrio", false, "int", "The log thread priority");
     370          216 :    config.add(m_configSection+".logLevel","l", "logLevel",mx::app::argType::Required, m_configSection, "logLevel", false, "string", "The log level");
     371              : 
     372           24 :    return 0;
     373              : }
     374              : 
     375              : template<class parentT, class logFileT>
     376           23 : int logManager<parentT, logFileT>::loadConfig( mx::app::appConfigurator & config )
     377              : {
     378              :    //-- logDir
     379           23 :    std::string tmp;
     380           23 :    config(tmp, m_configSection+".logDir");
     381           23 :    if(tmp != "") this->logPath(tmp);
     382              : 
     383              :    //-- logLevel
     384           23 :    tmp = "";
     385           23 :    config(tmp, m_configSection+".logLevel");
     386           23 :    if(tmp != "")
     387              :    {
     388              :       logPrioT lev;
     389              : 
     390            0 :       lev = logLevelFromString(tmp);
     391              : 
     392            0 :       if(  lev == logPrio::LOG_DEFAULT ) lev = logPrio::LOG_INFO;
     393            0 :       if( lev == logPrio::LOG_UNKNOWN )
     394              :       {
     395            0 :          std::cerr << "Unkown log level specified.  Using default (INFO)\n";
     396            0 :          lev = logPrio::LOG_INFO;
     397              :       }
     398            0 :       logLevel(lev);
     399              :    }
     400              : 
     401              : 
     402              :    //logExt
     403           23 :    config(this->m_logExt, m_configSection+".logExt");
     404              : 
     405              :    //maxLogSize
     406           23 :    config(this->m_maxLogSize, m_configSection+".maxLogSize");
     407              : 
     408              :    //writePause
     409           23 :    config(m_writePause, m_configSection+".writePause");
     410              : 
     411              :    //logThreadPrio
     412           23 :    config(m_logThreadPrio, m_configSection+".logThreadPrio");
     413              : 
     414           23 :    return 0;
     415           23 : }
     416              : 
     417              : template<class parentT, class logFileT>
     418            9 : void logManager<parentT, logFileT>::_logThreadStart( logManager * l)
     419              : {
     420            9 :    l->logThreadExec();
     421            9 : }
     422              : 
     423              : template<class parentT, class logFileT>
     424            9 : int logManager<parentT, logFileT>::logThreadStart()
     425              : {
     426              :    try
     427              :    {
     428            9 :       m_logThread = std::thread( _logThreadStart, this);
     429              :    }
     430            0 :    catch( const std::exception & e )
     431              :    {
     432            0 :       log<software_error>({__FILE__,__LINE__, 0, 0, std::string("Exception on log thread start: ") + e.what()});
     433            0 :       return -1;
     434              :    }
     435            0 :    catch( ... )
     436              :    {
     437            0 :       log<software_error>({__FILE__,__LINE__, 0, 0, "Unkown exception on log thread start"});
     438            0 :       return -1;
     439              :    }
     440              : 
     441            9 :    if(!m_logThread.joinable())
     442              :    {
     443            0 :       log<software_error>({__FILE__, __LINE__, 0, 0,  "Log thread did not start"});
     444            0 :       return -1;
     445              :    }
     446              : 
     447              :    //Always set the m_logThread to lowest priority
     448              :    sched_param sp;
     449            9 :    sp.sched_priority = m_logThreadPrio;
     450              : 
     451            9 :    int rv = pthread_setschedparam( m_logThread.native_handle(), SCHED_OTHER, &sp);
     452              : 
     453            9 :    if(rv != 0)
     454              :    {
     455            0 :       log<software_error>({__FILE__, __LINE__, 0, rv, std::string("Error setting thread params: ") + strerror(rv)});
     456            0 :       return -1;
     457              :    }
     458              : 
     459            9 :    return 0;
     460              : 
     461              : }
     462              : 
     463              : template<class parentT, class logFileT>
     464           36 : void logManager<parentT, logFileT>::logThreadExec()
     465              : {
     466              : 
     467           36 :    m_logThreadRunning = true;
     468              : 
     469           36 :    std::unique_lock<std::mutex> lock(m_qMutex, std::defer_lock);
     470              : 
     471              :    //Run until shutdown.  If shutdown is true we keep running while the queue is not empty.
     472          104 :    while(!m_logShutdown || !m_logQueue.empty())
     473              :    {
     474           68 :       std::list<bufferPtrT>::iterator beg, it, er, end;
     475              : 
     476              :       //Get begin and end once, so we only process logs in the list at this point.
     477              :       //Do it locked so we don't have any collisions with new logs (maybe not necessary)
     478           68 :       lock.lock();
     479           68 :       beg = m_logQueue.begin();
     480           68 :       end = m_logQueue.end();
     481           68 :       lock.unlock();
     482              : 
     483              :       //Note: we're avoiding use of size() since that could be altered concurrently by a push_back.
     484           68 :       if( beg != end )
     485              :       {
     486           42 :          it = beg;
     487         1469 :          while( it != end )
     488              :          {
     489              :             //m_logFile.
     490         1427 :             mx::error_t errc = this->writeLog( *it );
     491         1427 :             if( errc != mx::error_t::noerror)
     492              :             {
     493            0 :                m_logThreadRunning = false;
     494            0 :                return;
     495              :             }
     496              : 
     497         1427 :             if(m_parent)
     498              :             {
     499          152 :                m_parent->logMessage( *it );
     500              :             }
     501         1275 :             else if( logHeader::logLevel( *it ) <= logPrio::LOG_NOTICE )
     502              :             {
     503          577 :                logStdFormat(std::cerr, *it);
     504          577 :                std::cerr << '\n';
     505              :             }
     506              : 
     507         1427 :             er = it;
     508         1427 :             ++it;
     509              : 
     510              :             //Erase while locked so we don't collide with a push_back.
     511         1427 :             lock.lock();
     512         1427 :             m_logQueue.erase(er);
     513         1427 :             lock.unlock();
     514              :          }
     515              :       }
     516              : 
     517              :       //m_logFile.
     518              :       ///\todo must check this for errors, and investigate how `fsyncgate` impacts us
     519           68 :       this->flush();
     520              : 
     521              :       //We only pause if there's nothing to do.
     522           68 :       if(m_logQueue.empty() && !m_logShutdown) std::this_thread::sleep_for( std::chrono::duration<unsigned long, std::nano>(m_writePause));
     523              :    }
     524              : 
     525           36 :    m_logThreadRunning = false;
     526           36 : }
     527              : 
     528              : template<class parentT, class logFileT>
     529              : template<typename logT>
     530         1427 : int logManager<parentT, logFileT>::createLog( bufferPtrT & logBuffer,
     531              :                                      const typename logT::messageT & msg,
     532              :                                      const logPrioT & level
     533              :                                    )
     534              : {
     535              :    //Very first step is to get the current time.
     536         1427 :    timespecX ts;
     537         1427 :    ts.gettime();
     538              : 
     539         2854 :    return logHeader::createLog<logT>(logBuffer, ts, msg, level);
     540              : }
     541              : 
     542              : template<class parentT, class logFileT>
     543              : template<typename logT>
     544            0 : int logManager<parentT, logFileT>::createLog( bufferPtrT & logBuffer,
     545              :                                               const timespecX & ts,
     546              :                                               const typename logT::messageT & msg,
     547              :                                               const logPrioT & level
     548              :                                             )
     549              : {
     550            0 :    return logHeader::createLog<logT>(logBuffer, ts, msg, level);
     551              : }
     552              : 
     553              : template<class parentT, class logFileT>
     554              : template<typename logT>
     555         1449 : void logManager<parentT, logFileT>::log( const typename logT::messageT & msg,
     556              :                                          logPrioT level
     557              :                                        )
     558              : {
     559              :    //Step 0 check level.
     560         1449 :    if(level == logPrio::LOG_DEFAULT) level = logT::defaultLevel;
     561              : 
     562         1449 :    if(level > m_logLevel) return; // We do nothing with this.
     563              : 
     564              :    //Step 1 create log
     565         1427 :    bufferPtrT logBuffer;
     566         1427 :    createLog<logT>(logBuffer, msg, level);
     567              : 
     568              :    //Step 2 add log to queue
     569         1427 :    std::lock_guard<std::mutex> guard(m_qMutex);  //Lock the mutex before pushing back.
     570         1427 :    m_logQueue.push_back(logBuffer);
     571              : 
     572              : 
     573         1427 : }
     574              : 
     575              : template<class parentT, class logFileT>
     576              : template<typename logT>
     577            0 : void logManager<parentT, logFileT>::log( timespecX & ts,
     578              :                                          const typename logT::messageT & msg,
     579              :                                          logPrioT level
     580              :                                        )
     581              : {
     582              :    //Step 0 check level.
     583            0 :    if(level == logPrio::LOG_DEFAULT) level = logT::defaultLevel;
     584              : 
     585            0 :    if(level > m_logLevel) return; // We do nothing with this.
     586              : 
     587              :    //Step 1 create log
     588            0 :    bufferPtrT logBuffer;
     589            0 :    createLog<logT>(logBuffer, ts, msg, level);
     590              : 
     591              :    //Step 2 add log to queue
     592            0 :    std::lock_guard<std::mutex> guard(m_qMutex);  //Lock the mutex before pushing back.
     593            0 :    m_logQueue.push_back(logBuffer);
     594              : 
     595            0 : }
     596              : 
     597              : template<class parentT, class logFileT>
     598              : template<typename logT>
     599            6 : void logManager<parentT, logFileT>::log( logPrioT level )
     600              : {
     601            6 :    log<logT>( emptyMessage(), level );
     602            6 : }
     603              : 
     604              : template<class parentT, class logFileT>
     605              : template<typename logT>
     606              : void logManager<parentT, logFileT>::log( timespecX & ts,
     607              :                                 logPrioT level
     608              :                               )
     609              : {
     610              :    log<logT>( ts, emptyMessage(), level );
     611              : }
     612              : 
     613              : //class logFileRaw;
     614              : 
     615              : //extern template struct logManager<logFileRaw>;
     616              : 
     617              : } //namespace logger
     618              : } //namespace MagAOX
     619              : 
     620              : #endif //logger_logger_hpp
        

Generated by: LCOV version 2.0-1