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());