diff --git a/Foundation/Foundation_vs160.vcxproj b/Foundation/Foundation_vs160.vcxproj index 442fa14e2e..32ac92c199 100644 --- a/Foundation/Foundation_vs160.vcxproj +++ b/Foundation/Foundation_vs160.vcxproj @@ -873,6 +873,7 @@ + @@ -1637,6 +1638,7 @@ + diff --git a/Foundation/Foundation_vs160.vcxproj.filters b/Foundation/Foundation_vs160.vcxproj.filters index dbaf9eb242..fa7089179e 100644 --- a/Foundation/Foundation_vs160.vcxproj.filters +++ b/Foundation/Foundation_vs160.vcxproj.filters @@ -940,6 +940,9 @@ Text\Utf8Proc Source Files + + Logging\Source Files + @@ -1896,6 +1899,9 @@ Text\Utf8Proc Header Files + + Logging\Header Files + diff --git a/Foundation/Foundation_vs170.vcxproj b/Foundation/Foundation_vs170.vcxproj index c6232b03be..0689fc4f90 100644 --- a/Foundation/Foundation_vs170.vcxproj +++ b/Foundation/Foundation_vs170.vcxproj @@ -1275,6 +1275,7 @@ + @@ -2255,6 +2256,7 @@ + diff --git a/Foundation/Foundation_vs170.vcxproj.filters b/Foundation/Foundation_vs170.vcxproj.filters index b6d0235e92..c9fc589173 100644 --- a/Foundation/Foundation_vs170.vcxproj.filters +++ b/Foundation/Foundation_vs170.vcxproj.filters @@ -940,6 +940,9 @@ Text\Utf8Proc Source Files + + Logging\Source Files + @@ -1896,6 +1899,9 @@ Text\Utf8Proc Header Files + + Logging\Header Files + diff --git a/Foundation/Makefile b/Foundation/Makefile index 3b6b6e93ae..86fe425db3 100644 --- a/Foundation/Makefile +++ b/Foundation/Makefile @@ -19,7 +19,7 @@ objects = ArchiveStrategy Ascii ASCIIEncoding AsyncChannel AsyncNotificationCent NestedDiagnosticContext Notification NotificationCenter \ NotificationQueue PriorityNotificationQueue TimedNotificationQueue \ NullStream NumberFormatter NumberParser NumericString AbstractObserver \ - Path PatternFormatter PIDFile Process ProcessRunner PurgeStrategy RWLock Random RandomStream \ + Path PatternFormatter JSONFormatter PIDFile Process ProcessRunner PurgeStrategy RWLock Random RandomStream \ DirectoryIteratorStrategy RegularExpression RefCountedObject Runnable RotateStrategy \ SHA1Engine SHA2Engine Semaphore SharedLibrary SimpleFileChannel \ SignalHandler SplitterChannel SortedDirectoryIterator Stopwatch StreamChannel \ diff --git a/Foundation/include/Poco/JSONFormatter.h b/Foundation/include/Poco/JSONFormatter.h new file mode 100644 index 0000000000..5edc6e16d6 --- /dev/null +++ b/Foundation/include/Poco/JSONFormatter.h @@ -0,0 +1,109 @@ +// +// JSONFormatter.h +// +// Library: Foundation +// Package: Logging +// Module: JSONFormatter +// +// Definition of the JSONFormatter class. +// +// Copyright (c) 2024, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#ifndef Foundation_JSONFormatter_INCLUDED +#define Foundation_JSONFormatter_INCLUDED + + +#include "Poco/Foundation.h" +#include "Poco/Formatter.h" +#include "Poco/Message.h" +#include + + +namespace Poco { + + +class Foundation_API JSONFormatter: public Formatter + /// This formatter formats log messages as compact + /// (no unnecessary whitespace) single-line JSON strings. + /// + /// The following JSON schema is used: + /// { + /// "timestamp": "2024-09-26T13:41:23.324461Z", + /// "source": "sample", + /// "level": "information", + /// "message": "This is a test message.", + /// "thread": 12, + /// "file": "source.cpp", + /// "line": 456, + /// "params": { + /// "prop1": "value1" + /// } + /// } + /// + /// The "file" and "line" properties will only be included if the log + /// message contains a file name and line number. + /// + /// The "params" object will only be included if custom parameters + /// have been added to the Message. +{ +public: + using Ptr = AutoPtr; + + JSONFormatter() = default; + /// Creates a JSONFormatter. + + ~JSONFormatter() = default; + /// Destroys the JSONFormatter. + + void format(const Message& msg, std::string& text); + /// Formats the message as a JSON string. + + void setProperty(const std::string& name, const std::string& value); + /// Sets the property with the given name to the given value. + /// + /// The following properties are supported: + /// + /// * times: Specifies whether times are adjusted for local time + /// or taken as they are in UTC. Supported values are "local" and "UTC". + /// * thread: Specifies the value given for the thread. Can be + /// "none" (excluded), "name" (thread name), "id" (POCO thread ID) or "osid" + /// (operating system thread ID). + /// + /// If any other property name is given, a PropertyNotSupported + /// exception is thrown. + + std::string getProperty(const std::string& name) const; + /// Returns the value of the property with the given name or + /// throws a PropertyNotSupported exception if the given + /// name is not recognized. + + static const std::string PROP_TIMES; + static const std::string PROP_THREAD; + +protected: + std::string getThread(const Message& message) const; + static const std::string& getPriorityName(int prio); + + enum ThreadFormat + { + JSONF_THREAD_NONE = 0, + JSONF_THREAD_NAME = 1, + JSONF_THREAD_ID = 2, + JSONF_THREAD_OS_ID = 3 + }; + +private: + bool _localTime = false; + ThreadFormat _threadFormat = JSONF_THREAD_ID; +}; + + +} // namespace Poco + + +#endif // Foundation_JSONFormatter_INCLUDED diff --git a/Foundation/src/JSONFormatter.cpp b/Foundation/src/JSONFormatter.cpp new file mode 100644 index 0000000000..9b14487a95 --- /dev/null +++ b/Foundation/src/JSONFormatter.cpp @@ -0,0 +1,182 @@ +// +// JSONFormatter.cpp +// +// Library: Foundation +// Package: Logging +// Module: JSONFormatter +// +// Copyright (c) 2024, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#include "Poco/JSONFormatter.h" +#include "Poco/JSONString.h" +#include "Poco/Message.h" +#include "Poco/String.h" +#include "Poco/JSONString.h" +#include "Poco/NumberFormatter.h" +#include "Poco/DateTimeFormatter.h" +#include "Poco/DateTimeFormat.h" +#include "Poco/Timezone.h" + + +namespace Poco { + + +const std::string JSONFormatter::PROP_TIMES("times"); +const std::string JSONFormatter::PROP_THREAD("thread"); + + +void JSONFormatter::format(const Message& msg, std::string& text) +{ + Timestamp timestamp = msg.getTime(); + int tzd = DateTimeFormatter::UTC; + if (_localTime) + { + tzd = Timezone::utcOffset(); + tzd += Timezone::dst(); + timestamp += tzd*Timestamp::resolution(); + } + + text += '{'; + text += "\"timestamp\":\""; + text += Poco::DateTimeFormatter::format(timestamp, Poco::DateTimeFormat::ISO8601_FRAC_FORMAT, tzd); + text += "\",\"source\":"; + text += toJSON(msg.getSource()); + text += ",\"level\":\""; + text += getPriorityName(msg.getPriority()); + text += "\",\"message\":"; + text += toJSON(msg.getText()); + if (_threadFormat != JSONF_THREAD_NONE) + { + text += ",\"thread\":"; + text += getThread(msg); + } + if (msg.getSourceFile()) + { + text += ",\"file\":"; + text += toJSON(msg.getSourceFile()); + } + if (msg.getSourceLine()) + { + text += ",\"line\":\""; + text += Poco::NumberFormatter::format(msg.getSourceLine()); + text += "\""; + } + if (!msg.getAll().empty()) + { + text += ",\"params\":{"; + const auto& props = msg.getAll(); + bool first = true; + for (const auto& p: props) + { + if (!first) + text += ','; + else + first = false; + text += toJSON(p.first); + text += ':'; + text += toJSON(p.second); + } + text += '}'; + } + text += '}'; +} + + +void JSONFormatter::setProperty(const std::string& name, const std::string& value) +{ + if (name == PROP_TIMES) + { + if (Poco::icompare(value, "local"s) == 0) + _localTime = true; + else if (Poco::icompare(value, "utc"s) == 0) + _localTime = false; + else + throw Poco::InvalidArgumentException("Invalid times value (must be local or UTC)"s, value); + } + else if (name == PROP_THREAD) + { + if (Poco::icompare(value, "none"s) == 0) + _threadFormat = JSONF_THREAD_NONE; + else if (Poco::icompare(value, "name"s) == 0) + _threadFormat = JSONF_THREAD_NAME; + else if (Poco::icompare(value, "id"s) == 0) + _threadFormat = JSONF_THREAD_ID; + else if (Poco::icompare(value, "osid"s) == 0) + _threadFormat = JSONF_THREAD_OS_ID; + else + throw Poco::InvalidArgumentException("Invalid thread value (must be name, id or osID)"s, value); + } + else throw Poco::PropertyNotSupportedException(name); +} + + +std::string JSONFormatter::getProperty(const std::string& name) const +{ + if (name == PROP_TIMES) + { + return _localTime ? "local"s : "UTC"s; + } + else if (name == PROP_THREAD) + { + switch (_threadFormat) + { + case JSONF_THREAD_NONE: + return "none"s; + case JSONF_THREAD_NAME: + return "name"s; + case JSONF_THREAD_ID: + return "id"s; + case JSONF_THREAD_OS_ID: + return "osID"s; + default: + return "invalid"s; + } + } + else throw Poco::PropertyNotSupportedException(name); +} + + +std::string JSONFormatter::getThread(const Message& message) const +{ + switch (_threadFormat) + { + case JSONF_THREAD_NONE: + return ""s; + case JSONF_THREAD_NAME: + return toJSON(message.getThread()); + case JSONF_THREAD_ID: + return Poco::NumberFormatter::format(message.getTid()); + case JSONF_THREAD_OS_ID: + return Poco::NumberFormatter::format(message.getOsTid()); + default: + return ""s; + } +} + + +const std::string& JSONFormatter::getPriorityName(int prio) +{ + static const std::string PRIORITY_NAMES[] = { + "none"s, + "fatal"s, + "critical"s, + "error"s, + "warning"s, + "notice"s, + "information"s, + "debug"s, + "trace" + }; + + poco_assert (prio >= Message::PRIO_FATAL && prio <= Message::PRIO_TRACE); + + return PRIORITY_NAMES[prio]; +} + + +} // namespace Poco diff --git a/Foundation/src/LoggingFactory.cpp b/Foundation/src/LoggingFactory.cpp index 049c49e50e..6563d22b2f 100644 --- a/Foundation/src/LoggingFactory.cpp +++ b/Foundation/src/LoggingFactory.cpp @@ -29,6 +29,7 @@ #include "Poco/WindowsConsoleChannel.h" #endif #include "Poco/PatternFormatter.h" +#include "Poco/JSONFormatter.h" using namespace std::string_literals; @@ -112,6 +113,7 @@ void LoggingFactory::registerBuiltins() #endif _formatterFactory.registerClass("PatternFormatter"s, new Instantiator); + _formatterFactory.registerClass("JSONFormatter"s, new Instantiator); } diff --git a/Foundation/testsuite/Makefile-Driver b/Foundation/testsuite/Makefile-Driver index 264fe2c155..0eb16a9f92 100644 --- a/Foundation/testsuite/Makefile-Driver +++ b/Foundation/testsuite/Makefile-Driver @@ -20,8 +20,8 @@ objects = ActiveMethodTest ActivityTest ActiveDispatcherTest \ NDCTest NotificationCenterTest NotificationQueueTest \ PriorityNotificationQueueTest TimedNotificationQueueTest \ NotificationsTestSuite NullStreamTest NumberFormatterTest \ - NumberParserTest PathTest PatternFormatterTest PBKDF2EngineTest ProcessRunnerTest RWLockTest \ - RandomStreamTest RandomTest RegularExpressionTest SHA1EngineTest SHA2EngineTest \ + NumberParserTest PathTest PatternFormatterTest JSONFormatterTest PBKDF2EngineTest ProcessRunnerTest \ + RWLockTest RandomStreamTest RandomTest RegularExpressionTest SHA1EngineTest SHA2EngineTest \ SemaphoreTest ConditionTest SharedLibraryTest SharedLibraryTestSuite \ SimpleFileChannelTest StopwatchTest \ StreamConverterTest StreamCopierTest StreamTokenizerTest \ diff --git a/Foundation/testsuite/TestSuite_vs160.vcxproj b/Foundation/testsuite/TestSuite_vs160.vcxproj index 1da0e5e618..41a1fa60af 100644 --- a/Foundation/testsuite/TestSuite_vs160.vcxproj +++ b/Foundation/testsuite/TestSuite_vs160.vcxproj @@ -674,6 +674,7 @@ + @@ -817,6 +818,7 @@ + diff --git a/Foundation/testsuite/TestSuite_vs160.vcxproj.filters b/Foundation/testsuite/TestSuite_vs160.vcxproj.filters index 0c534981ee..e26995529d 100644 --- a/Foundation/testsuite/TestSuite_vs160.vcxproj.filters +++ b/Foundation/testsuite/TestSuite_vs160.vcxproj.filters @@ -603,6 +603,9 @@ Threading\Source Files + + Logging\Source Files + @@ -1028,5 +1031,8 @@ Threading\Header Files + + Logging\Header Files + \ No newline at end of file diff --git a/Foundation/testsuite/TestSuite_vs170.vcxproj b/Foundation/testsuite/TestSuite_vs170.vcxproj index d91f6c533b..91e3d7910c 100644 --- a/Foundation/testsuite/TestSuite_vs170.vcxproj +++ b/Foundation/testsuite/TestSuite_vs170.vcxproj @@ -1007,6 +1007,7 @@ + @@ -1150,6 +1151,7 @@ + diff --git a/Foundation/testsuite/TestSuite_vs170.vcxproj.filters b/Foundation/testsuite/TestSuite_vs170.vcxproj.filters index 0c534981ee..e26995529d 100644 --- a/Foundation/testsuite/TestSuite_vs170.vcxproj.filters +++ b/Foundation/testsuite/TestSuite_vs170.vcxproj.filters @@ -603,6 +603,9 @@ Threading\Source Files + + Logging\Source Files + @@ -1028,5 +1031,8 @@ Threading\Header Files + + Logging\Header Files + \ No newline at end of file diff --git a/Foundation/testsuite/src/JSONFormatterTest.cpp b/Foundation/testsuite/src/JSONFormatterTest.cpp new file mode 100644 index 0000000000..d409f675b6 --- /dev/null +++ b/Foundation/testsuite/src/JSONFormatterTest.cpp @@ -0,0 +1,94 @@ +// +// JSONFormatterTest.cpp +// +// Copyright (c) 2024, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#include "JSONFormatterTest.h" +#include "CppUnit/TestCaller.h" +#include "CppUnit/TestSuite.h" +#include "Poco/JSONFormatter.h" +#include "Poco/Message.h" +#include "Poco/DateTime.h" + + +using Poco::JSONFormatter; +using Poco::Message; +using Poco::DateTime; + + +JSONFormatterTest::JSONFormatterTest(const std::string& name): CppUnit::TestCase(name) +{ +} + + +JSONFormatterTest::~JSONFormatterTest() +{ +} + + +void JSONFormatterTest::testJSONFormatter() +{ + Message msg; + JSONFormatter fmt; + msg.setSource("TestSource"); + msg.setText("Test message text"); + msg.setPid(1234); + msg.setTid(1); + msg.setThread("TestThread"); + msg.setPriority(Message::PRIO_ERROR); + msg.setTime(DateTime(2005, 1, 1, 14, 30, 15, 500).timestamp()); + + std::string result; + fmt.format(msg, result); + assertTrue (result == "{\"timestamp\":\"2005-01-01T14:30:15.500000Z\",\"source\":\"TestSource\",\"level\":\"error\",\"message\":\"Test message text\",\"thread\":1}"); + + msg.setText("Multi\nline\ntext"); + result.clear(); + fmt.format(msg, result); + assertTrue (result == "{\"timestamp\":\"2005-01-01T14:30:15.500000Z\",\"source\":\"TestSource\",\"level\":\"error\",\"message\":\"Multi\\nline\\ntext\",\"thread\":1}"); + + fmt.setProperty("thread", "none"); + result.clear(); + fmt.format(msg, result); + assertTrue (result == "{\"timestamp\":\"2005-01-01T14:30:15.500000Z\",\"source\":\"TestSource\",\"level\":\"error\",\"message\":\"Multi\\nline\\ntext\"}"); + + msg.set("p1", "v1"); + result.clear(); + fmt.format(msg, result); + assertTrue (result == "{\"timestamp\":\"2005-01-01T14:30:15.500000Z\",\"source\":\"TestSource\",\"level\":\"error\",\"message\":\"Multi\\nline\\ntext\",\"params\":{\"p1\":\"v1\"}}"); + + msg.set("p2", "v2"); + result.clear(); + fmt.format(msg, result); + assertTrue (result == "{\"timestamp\":\"2005-01-01T14:30:15.500000Z\",\"source\":\"TestSource\",\"level\":\"error\",\"message\":\"Multi\\nline\\ntext\",\"params\":{\"p1\":\"v1\",\"p2\":\"v2\"}}"); + + fmt.setProperty("thread", "name"); + result.clear(); + fmt.format(msg, result); + assertTrue (result == "{\"timestamp\":\"2005-01-01T14:30:15.500000Z\",\"source\":\"TestSource\",\"level\":\"error\",\"message\":\"Multi\\nline\\ntext\",\"thread\":\"TestThread\",\"params\":{\"p1\":\"v1\",\"p2\":\"v2\"}}"); +} + + +void JSONFormatterTest::setUp() +{ +} + + +void JSONFormatterTest::tearDown() +{ +} + + +CppUnit::Test* JSONFormatterTest::suite() +{ + CppUnit::TestSuite* pSuite = new CppUnit::TestSuite("JSONFormatterTest"); + + CppUnit_addTest(pSuite, JSONFormatterTest, testJSONFormatter); + + return pSuite; +} diff --git a/Foundation/testsuite/src/JSONFormatterTest.h b/Foundation/testsuite/src/JSONFormatterTest.h new file mode 100644 index 0000000000..ae18b5dbcf --- /dev/null +++ b/Foundation/testsuite/src/JSONFormatterTest.h @@ -0,0 +1,38 @@ +// +// JSONFormatterTest.h +// +// Definition of the JSONFormatterTest class. +// +// Copyright (c) 2024, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#ifndef JSONFormatterTest_INCLUDED +#define JSONFormatterTest_INCLUDED + + +#include "Poco/Foundation.h" +#include "CppUnit/TestCase.h" + + +class JSONFormatterTest: public CppUnit::TestCase +{ +public: + JSONFormatterTest(const std::string& name); + ~JSONFormatterTest(); + + void testJSONFormatter(); + + void setUp(); + void tearDown(); + + static CppUnit::Test* suite(); + +private: +}; + + +#endif // JSONFormatterTest_INCLUDED diff --git a/Foundation/testsuite/src/LoggingTestSuite.cpp b/Foundation/testsuite/src/LoggingTestSuite.cpp index e55e9feb78..2e365fdf8f 100644 --- a/Foundation/testsuite/src/LoggingTestSuite.cpp +++ b/Foundation/testsuite/src/LoggingTestSuite.cpp @@ -12,6 +12,7 @@ #include "LoggerTest.h" #include "ChannelTest.h" #include "PatternFormatterTest.h" +#include "JSONFormatterTest.h" #include "FileChannelTest.h" #include "SimpleFileChannelTest.h" #include "LoggingFactoryTest.h" @@ -26,6 +27,7 @@ CppUnit::Test* LoggingTestSuite::suite() pSuite->addTest(LoggerTest::suite()); pSuite->addTest(ChannelTest::suite()); pSuite->addTest(PatternFormatterTest::suite()); + pSuite->addTest(JSONFormatterTest::suite()); pSuite->addTest(FileChannelTest::suite()); pSuite->addTest(SimpleFileChannelTest::suite()); pSuite->addTest(LoggingFactoryTest::suite());