Skip to content
Snippets Groups Projects
llerror.cpp 43.2 KiB
Newer Older
James Cook's avatar
James Cook committed
/** 
 * @file llerror.cpp
 * @date   December 2006
 * @brief error message system
James Cook's avatar
James Cook committed
 *
 * $LicenseInfo:firstyear=2006&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
James Cook's avatar
James Cook committed
 */
James Cook's avatar
James Cook committed
#include "linden_common.h"

#include "llerror.h"
#include "llerrorcontrol.h"

#include <cctype>
# include <cxxabi.h>
#endif // __GNUC__
#include <sstream>
#if !LL_WINDOWS
# include <syslog.h>
# include <unistd.h>
#endif // !LL_WINDOWS
#include <vector>
#include "string.h"
#include <absl/container/flat_hash_map.h>
#include <absl/strings/str_format.h>
#include "llapp.h"
#include "llapr.h"
#include "llfile.h"
#include "lllivefile.h"
#include "llsd.h"
#include "llsdserialize.h"
// On Mac, got:
// #error "Boost.Stacktrace requires `_Unwind_Backtrace` function. Define
// `_GNU_SOURCE` macro or `BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED` if
// _Unwind_Backtrace is available without `_GNU_SOURCE`."
#define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED
#if LL_WINDOWS
// On Windows, header-only implementation causes macro collisions -- use
// prebuilt library
#define BOOST_STACKTRACE_LINK
#endif // LL_WINDOWS
namespace {
#if LL_WINDOWS
	void debugger_print(const std::string& s)
	{
		// Be careful when calling OutputDebugString as it throws DBG_PRINTEXCEPTION_C 
		// which works just fine under the windows debugger, but can cause users who
		// have enabled SEHOP exception chain validation to crash due to interactions
		// between the Win 32-bit exception handling and boost coroutine fiber stacks. BUG-2707
		//
		if (IsDebuggerPresent())
		{
			// Need UTF16 for Unicode OutputDebugString
			//
			if (s.size())
			{
				OutputDebugString(ll_convert_string_to_wide(s).c_str());
	class RecordToSyslog : public LLError::Recorder
	{
	public:
		RecordToSyslog(const std::string& identity)
			: mIdentity(identity)
		{
			openlog(mIdentity.c_str(), LOG_CONS|LOG_PID, LOG_LOCAL0);
				// we need to set the string from a local copy of the string
				// since apparanetly openlog expects the const char* to remain
				// valid even after it returns (presumably until closelog)
		}
		
		~RecordToSyslog()
		{
			closelog();
		}

        virtual bool enabled() override
        {
            return LLError::getEnabledLogTypesMask() & 0x01;
        }
        
		virtual void recordMessage(LLError::ELevel level,
		{
			int syslogPriority = LOG_CRIT;
			switch (level) {
				case LLError::LEVEL_DEBUG:	syslogPriority = LOG_DEBUG;	break;
				case LLError::LEVEL_INFO:	syslogPriority = LOG_INFO;	break;
				case LLError::LEVEL_WARN:	syslogPriority = LOG_WARNING; break;
				case LLError::LEVEL_ERROR:	syslogPriority = LOG_CRIT;	break;
				default:					syslogPriority = LOG_CRIT;
			}
			
			syslog(syslogPriority, "%s", message.c_str());
		}
	private:
		std::string mIdentity;
	};
#endif

	class RecordToFile : public LLError::Recorder
	{
	public:
		RecordToFile(const std::string& filename):
			showMultiline(true);

			mFile.open(filename, std::ios_base::out | std::ios_base::app);
			if (!mFile)
			{
				LL_INFOS() << "Error setting log file to " << filename << LL_ENDL;
				if (!LLError::getAlwaysFlush())
				{
					mFile.sync_with_stdio(false);
				}
		~RecordToFile()
		{
			mFile.close();
		}
        virtual bool enabled() override
        {
#ifdef LL_RELEASE_FOR_DOWNLOAD
            return 1;
#else
            return LLError::getEnabledLogTypesMask() & 0x02;
#endif
        }
        
        bool okay() const { return mFile.good(); }
        std::string getFilename() const { return mName; }
        virtual void recordMessage(LLError::ELevel level,
                                    const std::string& message) override
        {
            if (LLError::getAlwaysFlush())
            {
                mFile << message << std::endl;
            }
            else
            {
                mFile << message << "\n";
            }
	private:
		llofstream mFile;
	};
	
	
	class RecordToStderr : public LLError::Recorder
	{
	public:
		RecordToStderr(bool timestamp) : mUseANSI(checkANSI()) 
        virtual bool enabled() override
        {
            return LLError::getEnabledLogTypesMask() & 0x04;
        }
        
        LL_FORCE_INLINE std::string createANSI(const std::string& color)
        {
            std::string ansi_code;
            ansi_code  += '\033';
            ansi_code  += "[";
            ansi_code  += color;
            ansi_code += "m";
            return ansi_code;
        }

		virtual void recordMessage(LLError::ELevel level,
            static std::string s_ansi_error = createANSI("31"); // red
            static std::string s_ansi_warn  = createANSI("34"); // blue
            static std::string s_ansi_debug = createANSI("35"); // magenta

                writeANSI((level == LLError::LEVEL_ERROR) ? s_ansi_error :
                          (level == LLError::LEVEL_WARN)  ? s_ansi_warn :
                                                            s_ansi_debug, message);
            else
            {
                 fprintf(stderr, "%s\n", message.c_str());
            }
        LL_FORCE_INLINE void writeANSI(const std::string& ansi_code, const std::string& message)
            static std::string s_ansi_bold  = createANSI("1");  // bold
            static std::string s_ansi_reset = createANSI("0");  // reset
			// ANSI color code escape sequence, message, and reset in one fprintf call
            // Default all message levels to bold so we can distinguish our own messages from those dumped by subprocesses and libraries.
			fprintf(stderr, "%s%s%s\n%s", s_ansi_bold.c_str(), ansi_code.c_str(), message.c_str(), s_ansi_reset.c_str() );
		}
		{
			// Check whether it's okay to use ANSI; if stderr is
			// a tty then we assume yes.  Can be turned off with
			// the LL_NO_ANSI_COLOR env var.
			return (0 != isatty(2)) &&
				(NULL == getenv("LL_NO_ANSI_COLOR"));
James Cook's avatar
James Cook committed

	class RecordToFixedBuffer : public LLError::Recorder
	{
	public:
		RecordToFixedBuffer(LLLineBuffer* buffer)
            : mBuffer(buffer)
            {
                this->showMultiline(true);
                this->showTags(false);
                this->showLocation(false);
            }
        virtual bool enabled() override
        {
            return LLError::getEnabledLogTypesMask() & 0x08;
        }
        
		virtual void recordMessage(LLError::ELevel level,
	};

#if LL_WINDOWS
	class RecordToWinDebug: public LLError::Recorder
	{
	public:
		{
            this->showMultiline(true);
            this->showTags(false);
            this->showLocation(false);
        }
        virtual bool enabled() override
        {
            return LLError::getEnabledLogTypesMask() & 0x10;
        }
        
		virtual void recordMessage(LLError::ELevel level,
namespace
James Cook's avatar
James Cook committed
{
	std::string className(const std::type_info& type)
	{
		return LLError::Log::demangle(type.name());
	}
} // anonymous

namespace LLError
{
Rye Mutt's avatar
Rye Mutt committed
#if LL_WINDOWS
	std::string Log::demangle(const std::string_view mangled)
Rye Mutt's avatar
Rye Mutt committed
#else
	std::string Log::demangle(const char* mangled)
#endif
	{
#ifdef __GNUC__
		// GCC: type_info::name() returns a mangled class name,st demangle
		// passing nullptr, 0 forces allocation of a unique buffer we can free
		// fixing MAINT-8724 on OSX 10.14
		int status = -1;
		char* name = abi::__cxa_demangle(mangled, nullptr, 0, &status);
		std::string result(name ? name : mangled);
		free(name);
		return result;
		// Visual Studio: type_info::name() includes the text "class " at the start
		static constexpr auto class_prefix = "class "sv;
		static constexpr auto struct_prefix = "struct "sv;
		if (0 == mangled.compare(0, class_prefix.length(), class_prefix))
			return std::string(mangled.substr(class_prefix.length()));
		else if (0 == mangled.compare(0, struct_prefix.length(), struct_prefix))
		{
			return std::string(mangled.substr(struct_prefix.length()));
		}

		// huh, that's odd, we should see one or the other prefix -- but don't
		// try to log unless logging is already initialized
		// in Python, " or ".join(vector) -- but in C++, a PITB
		LL_DEBUGS() << "Did not see 'class' or 'struct' prefix on '"
			<< mangled << "'" << LL_ENDL;
		return std::string(mangled);
#else  // neither GCC nor Visual Studio
	std::string functionName(const std::string& preprocessor_name)
	{
#if LL_WINDOWS
		// DevStudio: the __FUNCTION__ macro string includes
		// the type and/or namespace prefixes

		std::string::size_type p = preprocessor_name.rfind(':');
		if (p == std::string::npos)
		{
			return preprocessor_name;
		}
		return preprocessor_name.substr(p + 1);

#else
		return preprocessor_name;
#endif
	}


	class LogControlFile : public LLLiveFile
	{
		LOG_CLASS(LogControlFile);
	
	public:
		static LogControlFile& fromDirectory(const std::string& user_dir, const std::string& app_dir);
		virtual bool loadFile();
		
	private:
		LogControlFile(const std::string &filename)
			: LLLiveFile(filename)
			{ }
	};

	LogControlFile& LogControlFile::fromDirectory(const std::string& user_dir, const std::string& app_dir)
        // NB: We have no abstraction in llcommon  for the "proper"
        // delimiter but it turns out that "/" works on all three platforms
		std::string file = user_dir + "/logcontrol-dev.xml";
		
		llstat stat_info;
		if (LLFile::stat(file, &stat_info)) {
			// NB: stat returns non-zero if it can't read the file, for example
			// if it doesn't exist.  LLFile has no better abstraction for 
			// testing for file existence.
			
			file = app_dir + "/logcontrol.xml";
		return * new LogControlFile(file);
			// NB: This instance is never freed
	bool LogControlFile::loadFile()
	{
		LLSD configuration;

		{
				LL_WARNS() << filename() << " failed to open file; not changing configuration" << LL_ENDL;
				return false;
			}

			if (LLSDSerialize::fromXML(configuration, file) == LLSDParser::PARSE_FAILURE)
			{
				LL_WARNS() << filename() << " parcing error; not changing configuration" << LL_ENDL;
				return false;
			if (! configuration || !configuration.isMap())
				LL_WARNS() << filename() << " missing, ill-formed, or simply undefined"
							" content; not changing configuration"
						<< LL_ENDL;
				return false;
			}
		}
		
		LLError::configure(configuration);
		LL_INFOS("LogControlFile") << "logging reconfigured from " << filename() << LL_ENDL;
	}


	typedef std::map<std::string, LLError::ELevel> LevelMap;
	typedef std::vector<LLError::RecorderPtr> Recorders;
	typedef std::vector<LLError::CallSite*> CallSiteVector;

    public:
        static Globals* getInstance();
    protected:
		Globals();
	public:
		std::ostringstream messageStream;
		bool messageStreamInUse;
		std::string mFatalMessage;

		void addCallSite(LLError::CallSite&);
		void invalidateCallSites();

	private:
		CallSiteVector callSites;
	};

	Globals::Globals()
		: messageStream(),
		messageStreamInUse(false),
		callSites()
	{
	}

    Globals* Globals::getInstance()
    {
        // According to C++11 Function-Local Initialization
        // of static variables is supposed to be thread safe
        // without risk of deadlocks.
        static Globals inst;

        return &inst;
    }

	void Globals::addCallSite(LLError::CallSite& site)
	{
		callSites.push_back(&site);
	}
	
	void Globals::invalidateCallSites()
	{
		for (CallSiteVector::const_iterator i = callSites.begin();
			 i != callSites.end();
			 ++i)
		{
		}
		
		callSites.clear();
	}
}

namespace LLError
{
	class SettingsConfig : public LLRefCount
	public:
		LevelMap                            mFunctionLevelMap;
		LevelMap                            mClassLevelMap;
		LevelMap                            mFileLevelMap;
		LevelMap                            mTagLevelMap;
		absl::flat_hash_map<std::string, unsigned int> mUniqueLogMessages;
		LLError::FatalFunction              mCrashFunction;
		LLError::TimeFunction               mTimeFunction;
	private:
		SettingsConfig();
	};

	typedef LLPointer<SettingsConfig> SettingsConfigPtr;

    public:
        static Settings* getInstance();
    protected:
		Settings();
	public:
		SettingsConfigPtr getSettingsConfig();
		void restore(SettingsStoragePtr pSettingsStorage);
	SettingsConfig::SettingsConfig()
		: LLRefCount(),
		mDefaultLevel(LLError::LEVEL_DEBUG),
		mFunctionLevelMap(),
		mClassLevelMap(),
		mFileLevelMap(),
		mTagLevelMap(),
		mUniqueLogMessages(),
		mCrashFunction(NULL),
		mTimeFunction(NULL),
		mRecorders(),
		mShouldLogCallCounter(0)
	Settings::Settings():
    Settings* Settings::getInstance()
    {
        // According to C++11 Function-Local Initialization
        // of static variables is supposed to be thread safe
        // without risk of deadlocks.
        static Settings inst;

        return &inst;
    }

	SettingsConfigPtr Settings::getSettingsConfig()
	{
		return mSettingsConfig;
		Globals::getInstance()->invalidateCallSites();
	SettingsStoragePtr Settings::saveAndReset()
James Cook's avatar
James Cook committed
	{
		SettingsStoragePtr oldSettingsConfig(mSettingsConfig.get());
		reset();
		return oldSettingsConfig;
	void Settings::restore(SettingsStoragePtr pSettingsStorage)
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr newSettingsConfig(dynamic_cast<SettingsConfig *>(pSettingsStorage.get()));
		mSettingsConfig = std::move(newSettingsConfig);
James Cook's avatar
James Cook committed
	}
James Cook's avatar
James Cook committed

maksymsproductengine's avatar
maksymsproductengine committed
namespace LLError
{
	CallSite::CallSite(ELevel level,
					const char* file,
					int line,
					const std::type_info& class_info, 
					const char* function, 
	:	mLevel(level), 
		mFile(file), 
		mLine(line),
		mClassInfo(class_info), 
		mFunction(function),
		mCached(false), 
		mShouldLog(false), 
		mPrintOnce(printOnce),
        case LEVEL_DEBUG: mLevelString = "DEBUG";   break;
        case LEVEL_INFO:  mLevelString = "INFO";    break;
        case LEVEL_WARN:  mLevelString = "WARNING"; break;
        case LEVEL_ERROR: mLevelString = "ERROR";   break;
        default:          mLevelString = "XXX";     break;
		mLocationString = absl::StrFormat("%s(%d)", abbreviateFile(mFile), mLine);
#if LL_WINDOWS
		// DevStudio: __FUNCTION__ already includes the full class name
#else
#if LL_LINUX
		// gross, but typeid comparison seems to always fail here with gcc4.1
Richard Linden's avatar
Richard Linden committed
		if (0 != strcmp(mClassInfo.name(), typeid(NoClassInfo).name()))
Richard Linden's avatar
Richard Linden committed
		if (mClassInfo != typeid(NoClassInfo))
Richard Linden's avatar
Richard Linden committed
			mFunctionString = className(mClassInfo) + "::";
		mFunctionString += std::string(mFunction);

		for (int i = 0; i < tag_count; i++)
		{
            if (strchr(tags[i], ' '))
            {
                LL_ERRS() << "Space is not allowed in a log tag at " << mLocationString << LL_ENDL;
            }
			mTags[i] = tags[i];
		}

        mTagString.append("#");
        // always construct a tag sequence; will be just a single # if no tag
			mTagString.append(mTags[i]);
            mTagString.append("#");

	void CallSite::invalidate()
namespace
{
#if LL_DARWIN
        // On Mac OS X, stderr from apps launched from the Finder goes to the
        // console log.  It's generally considered bad form to spam too much
        // there. That scenario can be detected by noticing that stderr is a
        // character device (S_IFCHR).

        // If stderr is a tty or a pipe, assume the user launched from the
        // command line or debugger and therefore wants to see stderr.
        if (isatty(STDERR_FILENO))
            return true;
        // not a tty, but might still be a pipe -- check
        struct stat st;
        if (fstat(STDERR_FILENO, &st) < 0)
        {
            // capture errno right away, before engaging any other operations
            auto errno_save = errno;
            // this gets called during log-system setup -- can't log yet!
            std::cerr << "shouldLogToStderr: fstat(" << STDERR_FILENO << ") failed, errno "
                      << errno_save << std::endl;
            // if we can't tell, err on the safe side and don't write stderr
            return false;
        }

        // fstat() worked: return true only if stderr is a pipe
        return ((st.st_mode & S_IFMT) == S_IFIFO);
#endif
	bool stderrLogWantsTime()
	{
#if LL_WINDOWS
		return false;
#else
		return true;
#endif
	}
	
	
	void commonInit(const std::string& user_dir, const std::string& app_dir, bool log_to_stderr = true)
		LLError::Settings::getInstance()->reset();
		LLError::setDefaultLevel(LLError::LEVEL_INFO);
		LLError::setAlwaysFlush(true);
		LLError::setEnabledLogTypesMask(0xFFFFFFFF);
		LLError::setFatalFunction(LLError::crashAndLoop);
		LLError::setTimeFunction(LLError::utcTime);

		// log_to_stderr is only false in the unit and integration tests to keep builds quieter
		if (log_to_stderr && shouldLogToStderr())
#if LL_WINDOWS
		LLError::RecorderPtr recordToWinDebug = boost::make_shared<RecordToWinDebug>();
		LLError::addRecorder(std::move(recordToWinDebug));
		LogControlFile& e = LogControlFile::fromDirectory(user_dir, app_dir);

		// NOTE: We want to explicitly load the file before we add it to the event timer
		// that checks for changes to the file.  Else, we're not actually loading the file yet,
		// and most of the initialization happens without any attention being paid to the
		// log control file.  Not to mention that when it finally gets checked later,
		// all log statements that have been evaluated already become dirty and need to be
		// evaluated for printing again.  So, make sure to call checkAndReload()
		// before addToEventTimer().
		e.checkAndReload();
James Cook's avatar
James Cook committed

namespace LLError
James Cook's avatar
James Cook committed
{
	void initForApplication(const std::string& user_dir, const std::string& app_dir, bool log_to_stderr)
		commonInit(user_dir, app_dir, log_to_stderr);
	void setFatalFunction(const FatalFunction& f)
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mCrashFunction = f;
	FatalFunction getFatalFunction()
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		return s->mCrashFunction;
	std::string getFatalMessage()
	{
		return Globals::getInstance()->mFatalMessage;
	}
	void setTimeFunction(TimeFunction f)
	{
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mTimeFunction = f;
	}

	void setDefaultLevel(ELevel level)
	{
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mDefaultLevel = level;
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		return s->mDefaultLevel;
	void setAlwaysFlush(bool flush)
	{
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mLogAlwaysFlush = flush;
	}

	bool getAlwaysFlush()
	{
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		return s->mLogAlwaysFlush;
	}

	void setEnabledLogTypesMask(U32 mask)
	{
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mEnabledLogTypesMask = mask;
	}

	U32 getEnabledLogTypesMask()
	{
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		return s->mEnabledLogTypesMask;
	}

	void setFunctionLevel(const std::string& function_name, ELevel level)
	{
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mFunctionLevelMap[function_name] = level;
	}

	void setClassLevel(const std::string& class_name, ELevel level)
	{
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mClassLevelMap[class_name] = level;
	}

	void setFileLevel(const std::string& file_name, ELevel level)
	{
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mFileLevelMap[file_name] = level;

	void setTagLevel(const std::string& tag_name, ELevel level)
	{
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mTagLevelMap[tag_name] = level;

	LLError::ELevel decodeLevel(std::string name)
	{
		static LevelMap level_names;
		if (level_names.empty())
		{
			level_names["ALL"]		= LLError::LEVEL_ALL;
			level_names["DEBUG"]	= LLError::LEVEL_DEBUG;
			level_names["INFO"]		= LLError::LEVEL_INFO;
			level_names["WARN"]		= LLError::LEVEL_WARN;
			level_names["ERROR"]	= LLError::LEVEL_ERROR;
			level_names["NONE"]		= LLError::LEVEL_NONE;
		}
		
		std::transform(name.begin(), name.end(), name.begin(), toupper);
		
		LevelMap::const_iterator i = level_names.find(name);
		if (i == level_names.end())
		{
			LL_WARNS() << "unrecognized logging level: '" << name << "'" << LL_ENDL;
			return LLError::LEVEL_INFO;
		}
		
		return i->second;
	}
	void setLevels(LevelMap& map, const LLSD& list, LLError::ELevel level)
	{
		LLSD::array_const_iterator i, end;
		for (i = list.beginArray(), end = list.endArray(); i != end; ++i)
		{
			map[*i] = level;
		}
	}
}

namespace LLError
{
	void configure(const LLSD& config)
	{
		Globals::getInstance()->invalidateCallSites();
		SettingsConfigPtr s = Settings::getInstance()->getSettingsConfig();
		s->mFunctionLevelMap.clear();
		s->mClassLevelMap.clear();
		s->mFileLevelMap.clear();
		s->mTagLevelMap.clear();
		s->mUniqueLogMessages.clear();
		
		setDefaultLevel(decodeLevel(config["default-level"]));
        if (config.has("log-always-flush"))
        {
            setAlwaysFlush(config["log-always-flush"]);
        }
        if (config.has("enabled-log-types-mask"))
        {
            setEnabledLogTypesMask(config["enabled-log-types-mask"].asInteger());
        }
        
        if (config.has("settings") && config["settings"].isArray())
        {
            LLSD sets = config["settings"];
            LLSD::array_const_iterator a, end;
            for (a = sets.beginArray(), end = sets.endArray(); a != end; ++a)
            {
                const LLSD& entry = *a;
                if (entry.isMap() && entry.size() != 0)
                {
                    ELevel level = decodeLevel(entry["level"]);

                    setLevels(s->mFunctionLevelMap, entry["functions"], level);
                    setLevels(s->mClassLevelMap, entry["classes"], level);
                    setLevels(s->mFileLevelMap, entry["files"], level);
                    setLevels(s->mTagLevelMap, entry["tags"], level);
                }
            }
        }
maksymsproductengine's avatar
maksymsproductengine committed
namespace LLError
{
    	: mWantsTime(true)
        , mWantsTags(true)
        , mWantsLevel(true)
        , mWantsLocation(true)
        , mWantsFunctionName(true)
        , mWantsMultiline(false)
	bool Recorder::wantsTime()
	// virtual 
	bool Recorder::wantsLevel() 
	{ 
		return mWantsLevel;
	}

	// virtual 
	bool Recorder::wantsLocation() 
	{ 
		return mWantsLocation;
	}

	// virtual 
	bool Recorder::wantsFunctionName() 
	{ 
		return mWantsFunctionName;
	// virtual 
	bool Recorder::wantsMultiline() 
	{ 
		return mWantsMultiline;