From a414935b03650d0c3c1a618861ce606b2e70f43c Mon Sep 17 00:00:00 2001 From: Shamser Ahmed Date: Fri, 10 May 2024 17:18:16 +0100 Subject: [PATCH] HPCC-31774 Implement API logging, hiding, deleting and querying global messages Signed-off-by: Shamser Ahmed --- common/CMakeLists.txt | 1 + common/sysinfologger/CMakeLists.txt | 58 +++ common/sysinfologger/sourcedoc.xml | 26 ++ common/sysinfologger/sysinfologger.cpp | 554 +++++++++++++++++++++++++ common/sysinfologger/sysinfologger.hpp | 51 +++ 5 files changed, 690 insertions(+) create mode 100644 common/sysinfologger/CMakeLists.txt create mode 100644 common/sysinfologger/sourcedoc.xml create mode 100644 common/sysinfologger/sysinfologger.cpp create mode 100644 common/sysinfologger/sysinfologger.hpp diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index eee9f141976..fbd203a00b9 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -31,3 +31,4 @@ HPCC_ADD_SUBDIRECTORY (pkgfiles) HPCC_ADD_SUBDIRECTORY (workunit) HPCC_ADD_SUBDIRECTORY (wuanalysis) HPCC_ADD_SUBDIRECTORY (wuwebview "PLATFORM") +HPCC_ADD_SUBDIRECTORY (sysinfologger) diff --git a/common/sysinfologger/CMakeLists.txt b/common/sysinfologger/CMakeLists.txt new file mode 100644 index 00000000000..8606af7f350 --- /dev/null +++ b/common/sysinfologger/CMakeLists.txt @@ -0,0 +1,58 @@ +################################################################################ +# HPCC SYSTEMS software Copyright (C) 2024 HPCC Systems®. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + + +# File : CMakeLists.txt +# Component: sysinfologger +##################################################### +# Description: +# ------------ +# Cmake input file for sysinfologger +##################################################### + +project ( sysinfologger ) + +set ( SRCS + sysinfologger.cpp + + sysinfologger.hpp + ) + +include_directories ( + ${HPCC_SOURCE_DIR}/system/jlib + ${HPCC_SOURCE_DIR}/system/include + ${HPCC_SOURCE_DIR}/system/jlog + ${HPCC_SOURCE_DIR}/system/mp + ${HPCC_SOURCE_DIR}/dali/base + ${HPCC_SOURCE_DIR}/testing/unittests + ) + +ADD_DEFINITIONS( -DSYSINFOMSG_EXPORTS ) + +HPCC_ADD_LIBRARY( sysinfologger SHARED ${SRCS} ) + +target_link_libraries ( + sysinfologger + jlib + dalibase + ${CppUnit_LIBRARIES} + ) + +if ( USE_CPPUNIT ) + target_link_libraries ( sysinfologger ) +endif() + +install ( TARGETS sysinfologger RUNTIME DESTINATION ${EXEC_DIR} LIBRARY DESTINATION ${LIB_DIR} ) diff --git a/common/sysinfologger/sourcedoc.xml b/common/sysinfologger/sourcedoc.xml new file mode 100644 index 00000000000..cd2f7e03e8c --- /dev/null +++ b/common/sysinfologger/sourcedoc.xml @@ -0,0 +1,26 @@ + + + +
+ common/sysinfologger + + + The common/sysinfologger directory contains the sources for the common/sysinfologger library. + +
diff --git a/common/sysinfologger/sysinfologger.cpp b/common/sysinfologger/sysinfologger.cpp new file mode 100644 index 00000000000..feb4a6eaf62 --- /dev/null +++ b/common/sysinfologger/sysinfologger.cpp @@ -0,0 +1,554 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2024 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +#include "sysinfologger.hpp" +#include "daclient.hpp" +#include "jutil.hpp" + +#ifdef _USE_CPPUNIT +#include +#include +#include +#endif + +#define SDS_LOCK_TIMEOUT (5*60*1000) // 5 minutes +#define SYS_INFO_VERSION "1.0" +#define BOOL_STR(b) (b?"true":"false") + +#define SYS_INFO_ROOT "/SysInfo" +#define MSG_NODE "msg" +#define ATTR_TIMESTAMP "@ts" +#define ATTR_AUDIENCE "@audience" +#define ATTR_CLASS "@class" +#define ATTR_CODE "@code" +#define ATTR_HIDDEN "@hidden" + + +static unsigned readDigits(char const * & p, unsigned numDigits) +{ + unsigned num = 0; + for (unsigned n=0; n msgPtree = nullptr; + +public: + CSysInfoLoggerMsg & set(IPropertyTree & ptree) + { + msgPtree.setown(&ptree); + return * this; + } + unsigned __int64 queryTimeStamp() const override + { + return msgPtree->getPropInt64(ATTR_TIMESTAMP); + } + LogMsgAudience queryAudience() const override + { + return LogMsgAudFromAbbrev(msgPtree->queryProp(ATTR_AUDIENCE)); + } + LogMsgClass queryClass() const override + { + return LogMsgClassFromAbbrev(msgPtree->queryProp(ATTR_CLASS)); + } + LogMsgCode queryLogMsgCode() const override + { + return msgPtree->getPropInt(ATTR_CODE); + } + const char * queryMsg() const override + { + return msgPtree->queryProp("."); + } + bool queryIsHidden() const override + { + return msgPtree->getPropBool(ATTR_HIDDEN); + } + static IPropertyTree * createMsgPTree(const LogMsgCategory & cat, LogMsgCode code, const char * msg, unsigned __int64 ts, bool hidden) + { + Owned msgPtree = createPTree(MSG_NODE); + msgPtree->setPropBool(ATTR_HIDDEN, false); + msgPtree->setPropInt64(ATTR_TIMESTAMP, ts); + msgPtree->setPropInt(ATTR_CODE, code); + msgPtree->setProp(ATTR_AUDIENCE, LogMsgAudienceToFixString(cat.queryAudience())); + msgPtree->setProp(ATTR_CLASS, LogMsgClassToFixString(cat.queryClass())); + msgPtree->setProp(".", msg); + return msgPtree.getClear(); + } + static StringBuffer &buildMsgMatchXpath(const LogMsgAudience aud, const LogMsgClass logClass, LogMsgCode code, unsigned __int64 ts, StringBuffer & xpath) + { + return xpath.appendf(MSG_NODE "[" ATTR_TIMESTAMP "='%" I64F "u'][" ATTR_CODE "='%d'][" ATTR_AUDIENCE "='%s'][" ATTR_CLASS "='%s']", ts, (int) code, LogMsgAudienceToFixString(aud), LogMsgClassToFixString(logClass) ); + } +}; + +class CSysInfoLoggerMsgIterator : public CInterface, implements ISysInfoLoggerMsgIterator +{ + Owned conn; + CSysInfoLoggerMsg infoMsg; + Owned msgIter; + bool hiddenOnly = false; + bool visibleOnly = false; + unsigned year, month, day; + + bool ensureMatch() + { + for (; msgIter && msgIter->isValid(); msgIter->next()) + { + IPropertyTree & pt = msgIter->query(); + if(day) + { + unsigned __int64 ts = pt.getPropInt64(ATTR_TIMESTAMP, 0); + if (!ts) + continue; + CDateTime dt; + dt.setTimeStamp(ts); + unsigned tyear, tmonth, tday; + dt.getDate(tyear, tmonth, tday); + if (tday!=day) + continue; + } + break; + } + return msgIter->isValid(); + } + +public: + IMPLEMENT_IINTERFACE; + + CSysInfoLoggerMsgIterator(bool _hiddenOnly, bool _visibleOnly, unsigned _year=0, unsigned _month=0, unsigned _day=0) + : hiddenOnly(_hiddenOnly), visibleOnly(_visibleOnly), year(_year), month(_month), day(_day) + { + if (hiddenOnly && visibleOnly) + throw makeStringExceptionV(-1, "CSysInfoLoggerMsgIterator: cannot filter by both hiddenOnly and visibleOnly"); + if (!month && day) + throw makeStringExceptionV(-1, "CSysInfoLoggerMsgIterator: month and year must be provided when filtering by day"); + if (!year && month) + throw makeStringExceptionV(-1, "CSysInfoLoggerMsgIterator: year must be provided when filtering by month"); + } + virtual ISysInfoLoggerMsg & query() override + { + return infoMsg.set(msgIter->get()); + } + virtual bool first() override + { + StringBuffer xpath(SYS_INFO_ROOT); + if (year && month) + { + xpath.appendf("/m%04d%02d", year, month); + if (day) + xpath.appendf("/d%02d", day); + } + conn.setown(querySDS().connect(xpath.str(), myProcessSession(), RTM_LOCK_READ, SDS_LOCK_TIMEOUT)); + + xpath.set("//" MSG_NODE); + if (hiddenOnly) + xpath.append("[@hidden='1')]"); + if (visibleOnly) + xpath.append("[@hidden='0')]"); + + msgIter.setown(conn->queryRoot()->getElements(xpath.str())); + msgIter->first(); + return ensureMatch(); + } + virtual bool next() override + { + msgIter->next(); + return ensureMatch(); + + } + virtual bool isValid() override + { + return msgIter ? msgIter->isValid() : false; + } +}; + +ISysInfoLoggerMsgIterator * createSysInfoLoggerMsgIterator(unsigned year, unsigned month, unsigned day, bool visibleOnly, bool hiddenOnly) +{ + return new CSysInfoLoggerMsgIterator(hiddenOnly, visibleOnly, year, month, day); +} + +void logSysInfoError(const LogMsgCategory & cat, LogMsgCode code, const char * msg, unsigned __int64 ts) +{ + if (ts==0) + ts = getTimeStampNowValue(); + + StringBuffer xpath; + getRootPath(ts, xpath); + Owned conn = querySDS().connect(xpath.str(), myProcessSession(), RTM_LOCK_WRITE|RTM_CREATE_QUERY, SDS_LOCK_TIMEOUT); + if (!conn) + throw makeStringExceptionV(-1, "logSysInfoError: unable to create connection to '%s'", xpath.str()); + IPropertyTree * root = conn->queryRoot(); + if (!root->hasProp("@version")) + root->addProp("@version", SYS_INFO_VERSION); + + root->addPropTree(MSG_NODE, CSysInfoLoggerMsg::createMsgPTree(cat, code, msg, ts, false)); + conn->close(); +}; + +bool hideLogSysInfoMsg(LogMsgCategory & cat, LogMsgCode code, unsigned __int64 ts) +{ + StringBuffer xpath; + getRootPath(ts, xpath); + CSysInfoLoggerMsg::buildMsgMatchXpath(cat.queryAudience(), cat.queryClass(), code, ts, xpath); + xpath.append("[1]"); // Match first message only + + Owned conn = querySDS().connect(xpath.str(), myProcessSession(), RTM_LOCK_READ | RTM_LOCK_WRITE, SDS_LOCK_TIMEOUT); + if (!conn) + throw makeStringExceptionV(-1, "logSysInfoError: unable to create connection to '%s'", xpath.str()); + conn->queryRoot()->setPropBool(ATTR_HIDDEN, true); + return true; +} + +bool deleteLogSysInfoMsg(LogMsgCategory & cat, LogMsgCode code, unsigned __int64 ts) +{ + StringBuffer xpath; + getRootPath(ts, xpath); + + Owned conn = querySDS().connect(xpath.str(), myProcessSession(), RTM_LOCK_READ | RTM_LOCK_WRITE, SDS_LOCK_TIMEOUT); + if (!conn) + throw makeStringExceptionV(-1, "logSysInfoError: unable to create connection to '%s'", xpath.str()); + + IPropertyTree * root = conn->queryRoot(); + CSysInfoLoggerMsg::buildMsgMatchXpath(cat.queryAudience(), cat.queryClass(), code, ts, xpath.clear()); + xpath.append("[1]"); // Match first message only + IPropertyTree * msgPtree = root->queryPropTree(xpath.str()); + if (msgPtree && root) + { + root->removeTree(msgPtree); + return true; + } + return false; +} + +unsigned deleteOlderThanLogSysInfoMsg(unsigned year, unsigned month, unsigned day, bool visibleOnly, bool hiddenOnly) +{ + unsigned removed = 0; + Owned conn = querySDS().connect(SYS_INFO_ROOT, myProcessSession(), RTM_LOCK_WRITE, SDS_LOCK_TIMEOUT); + Owned monthIter = conn->queryRoot()->getElements("./*"); + + ForEach(*monthIter) + { + IPropertyTree & monthPT = monthIter->query(); + unsigned msgYear = 0, msgMonth = 0; + const char *p = monthPT.queryName(); // should be in format 'myyyydd' + if (*p++ == 'm') + { + msgYear = readDigits(p, 4); + msgMonth= readDigits(p, 2); + } + if (msgYear == 0 || msgMonth == 0) + throw makeStringExceptionV(-1, "child of " SYS_INFO_ROOT " is invalid: %s", monthPT.queryName()); + if (msgYear > year) + continue; + if (msgYear == year && (msgMonth > month)) + continue; + if (msgMonth < month && !hiddenOnly && !visibleOnly) + { + conn->queryRoot()->removeTree(&monthPT); + ++removed; + } + else + { + Owned dayIter = monthPT.getElements("./*"); + ForEach(*dayIter) + { + IPropertyTree & dayPT = dayIter->query(); + unsigned msgDay = 0; + const char * d = dayPT.queryName(); + if (*d++ == 'd') + msgDay = readDigits(d, 2); + if (msgDay == 0) + throw makeStringExceptionV(-1, "child of " SYS_INFO_ROOT "/%s is invalid: %s", monthPT.queryName(), dayPT.queryName()); + if ((msgDay >= day)) + continue; + if (hiddenOnly||visibleOnly) + { + IArrayOf delmsgs; + Owned msgIter = dayPT.getElements("./*"); + ForEach(*msgIter) + { + IPropertyTree & msgPT = msgIter->query(); + bool isHidden = msgPT.getPropBool(ATTR_HIDDEN); + if ((hiddenOnly && !isHidden) || (visibleOnly && isHidden)) + continue; + delmsgs.append(msgIter->get()); + } + ForEachItemIn(d, delmsgs) + { + IPropertyTree & msgPT = delmsgs.item(d); + dayPT.removeTree(&msgPT); + removed++; + } + } + else + { + monthPT.removeTree(&dayPT); + ++removed; + } + } + } + } + return removed; +} + +#ifdef _USE_CPPUNIT +#include "unittests.hpp" + +std::atomic_bool initialized {false}; +CriticalSection crit; + +void daliClientInit() +{ + CriticalBlock b(crit); + if (initialized.load()==true) + return; + InitModuleObjects(); + SocketEndpoint ep; + ep.set(".", 7070); + SocketEndpointArray epa; + epa.append(ep); + Owned group = createIGroup(epa); + initClientProcess(group, DCR_Testing); + initialized.store(true); +} + +void daliClientEnd() +{ + CriticalBlock b(crit); + if (initialized.load()==false) + return; + closedownClientProcess(); + initialized.store(false); +} + +class CSysInfoLoggerTester : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(CSysInfoLoggerTester); + CPPUNIT_TEST(test); + CPPUNIT_TEST_SUITE_END(); + + unsigned testRead(bool hiddenOnly=false, bool visibleOnly=false, unsigned year=0, unsigned month=0, unsigned day=0) + { + unsigned readCount=0; + try + { + std::set matchedMessages; // used to make sure every message written has been read back + // Test cases for this day only + Owned iter = new CSysInfoLoggerMsgIterator(hiddenOnly, visibleOnly, year, month, day); + ForEach(*iter) + { + const ISysInfoLoggerMsg & sysInfoMsg = iter->query(); + + WrittenLogMessage wm{sysInfoMsg.queryTimeStamp(), 0}; + auto matched = std::find(writtenMessages.begin(), writtenMessages.end(), wm); + if (matched==writtenMessages.end()) + continue; // not a message written by this unittest so ignore + matchedMessages.insert(matched->index); + + TestCase & testCase = testCases[matched->index]; + ASSERT(testCase.hidden==sysInfoMsg.queryIsHidden()); + ASSERT(testCase.code==sysInfoMsg.queryLogMsgCode()); + ASSERT(strcmp(testCase.msg,sysInfoMsg.queryMsg())==0); + ASSERT(testCase.cat.queryAudience()==sysInfoMsg.queryAudience()); + ASSERT(testCase.cat.queryClass()==sysInfoMsg.queryClass()); + + readCount++; + } + ASSERT(readCount==matchedMessages.size()); // make sure there are not duplicates + } + catch (IException *e) + { + StringBuffer msg; + msg.appendf("testRead(hidden=%s, visible=%s) failed: ", BOOL_STR(hiddenOnly), BOOL_STR(visibleOnly)); + e->errorMessage(msg); + msg.appendf("(code %d)", e->errorCode()); + e->Release(); + CPPUNIT_FAIL(msg.str()); + } + return readCount; + } + + struct TestCase + { + LogMsgCategory cat; + LogMsgCode code; + bool hidden; + const char * dateTimeStamp; + const char * msg; + }; + + std::vector testCases = + { + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42301, + false, + "2000-02-03T10:01:22.342343", + "CSysInfoLogger Unit test message 1" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42302, + false, + "2000-02-03T12:03:42.114233", + "CSysInfoLogger Unit test message 2" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42303, + true, + "2000-02-03T14:02:13.678443", + "CSysInfoLogger Unit test message 3" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42304, + true, + "2000-02-03T16:05:18.8324832", + "CSysInfoLogger Unit test message 4" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42301, + false, + "2000-02-04T03:01:42.5754", + "CSysInfoLogger Unit test message 5" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42302, + false, + "2000-02-04T09:06:25.133132", + "CSysInfoLogger Unit test message 6" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42303, + false, + "2000-02-04T11:09:32.78439", + "CSysInfoLogger Unit test message 7" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42304, + true, + "2000-02-04T13:02:12.82821", + "CSysInfoLogger Unit test message 8" + }, + { + LogMsgCategory(MSGAUD_operator, MSGCLS_information, DefaultDetail), + 42304, + true, + "2000-02-04T18:32:11.23421", + "CSysInfoLogger Unit test message 9" + } + }; + + struct WrittenLogMessage + { + unsigned __int64 ts; + unsigned index; + inline bool operator==(const WrittenLogMessage& rhs) + { + return ts==rhs.ts; + } + }; + std::vector writtenMessages; + +public: + CSysInfoLoggerTester() + { + try + { + daliClientInit(); + } + catch (IException *e) + { + StringBuffer msg; + e->errorMessage(msg); + printf("daliClientInit failed: %s (code %d)", msg.str(), e->errorCode()); + e->Release(); + } + } + ~CSysInfoLoggerTester() + { + daliClientEnd(); + } + void testWrite() + { + unsigned index=0; + for (auto testCase: testCases) + { + try + { + CDateTime dateTime; + dateTime.setString(testCase.dateTimeStamp); + + unsigned __int64 ts = dateTime.getTimeStamp(); + logSysInfoError(testCase.cat, testCase.code, testCase.msg, ts); + writtenMessages.push_back({ts, index++}); + if (testCase.hidden) + ASSERT(hideLogSysInfoMsg(testCase.cat, testCase.code, ts)==true); + } + catch (IException *e) + { + StringBuffer msg; + msg.append("logSysInfoError failed: "); + e->errorMessage(msg); + msg.appendf("(code %d)", e->errorCode()); + e->Release(); + CPPUNIT_FAIL(msg.str()); + } + } + ASSERT(testCases.size()==writtenMessages.size()); + } + void test() + { + testWrite(); + ASSERT(testRead(false, false)==9); + ASSERT(testRead(false, false, 2000, 02, 03)==4); + ASSERT(testRead(false, false, 2000, 02, 04)==5); + ASSERT(testRead(false, true)==5); //visible only + ASSERT(testRead(true, false)==4); //hidden only + ASSERT(deleteOlderThanLogSysInfoMsg(2000, 02, 04, false, true)==2); + ASSERT(deleteOlderThanLogSysInfoMsg(2000, 02, 05, true, false)==5); + ASSERT(deleteOlderThanLogSysInfoMsg(2000, 02, 05, false, false)==2); + writtenMessages.clear(); + ASSERT(testRead(false, false)==0); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION( CSysInfoLoggerTester ); +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( CSysInfoLoggerTester, "CSysInfoLogger" ); + +#endif diff --git a/common/sysinfologger/sysinfologger.hpp b/common/sysinfologger/sysinfologger.hpp new file mode 100644 index 00000000000..8e2d417a609 --- /dev/null +++ b/common/sysinfologger/sysinfologger.hpp @@ -0,0 +1,51 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2024 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +#ifndef SYSINFOLOGGER +#define SYSINFOLOGGER + +#include "jlog.hpp" +#include "jutil.hpp" + +#ifdef SYSINFOMSG_EXPORTS + #define SYSINFO_API DECL_EXPORT +#else + #define SYSINFO_API DECL_IMPORT +#endif + +interface ISysInfoLoggerMsg +{ + virtual unsigned __int64 queryTimeStamp() const = 0; + virtual LogMsgAudience queryAudience() const = 0; + virtual LogMsgClass queryClass() const = 0; + virtual LogMsgCode queryLogMsgCode() const = 0; + virtual const char * queryMsg() const = 0; + virtual bool queryIsHidden() const = 0; +}; + +interface ISysInfoLoggerMsgIterator : implements IScmIterator +{ + virtual ISysInfoLoggerMsg & query() = 0; +}; + +SYSINFO_API ISysInfoLoggerMsgIterator * createSysInfoLoggerMsgIterator(unsigned year=0, unsigned month=0, unsigned day=0, bool visibleOnly=true, bool hiddenOnly=false); +SYSINFO_API void logSysInfoError(const LogMsgCategory & cat, LogMsgCode code, const char * msg, unsigned __int64 ts=0); +SYSINFO_API bool hideLogSysInfoMsg(LogMsgCategory & cat, LogMsgCode code, unsigned __int64 ts); +SYSINFO_API bool deleteLogSysInfoMsg(LogMsgCategory & cat, LogMsgCode code, unsigned __int64 ts); +SYSINFO_API unsigned deleteOlderThanLogSysInfoMsg(unsigned year=0, unsigned month=0, unsigned day=0, bool visibleOnly=true, bool hiddenOnly=true); + +#endif