Repository: fledge-iot/fledge
Pull request #1629: FOGL-9746 implementation of Remote UDP Logging for syslog
Author: login: ashwini-k-pandey
Created: 2025-05-05T19:00:30Z | Merged: 2025-05-21T16:59:05Z

### **Remote UDP Logging:**

1. **Added support for logging messages to a remote syslog via UDP protocol.**
The logging system initializes a UDP socket (m_UdpSockFD) and sends log messages to a remote server defined by environment variables.

1. **Environment Variables:**
Introduced the following environment variables for dynamic configuration:
**SYSLOG_UDP_ENABLED:** Enables or disables UDP logging. If set to true, logs are sent to the configured UDP sink.
**LOG_IP:** Specifies the IP address of the remote logging server. Defaults to 127.0.0.1 if not set.
**LOG_PORT:** Specifies the port number for the remote logging server. Defaults to 5140 if not set.
The environment variables allow customization without requiring code changes, making the system adaptable to different deployment environments



Diff:
diff --git a/C/common/include/logger.h b/C/common/include/logger.h
index aaf90ac116..0a975451bb 100755
--- a/C/common/include/logger.h
+++ b/C/common/include/logger.h
@@ -18,7 +18,8 @@
 #include <thread>
 #include <condition_variable>
 #include <atomic>
-
+#include <sys/socket.h>
+#include <arpa/inet.h>
 #define PRINT_FUNC	Logger::getLogger()->info("%s:%d", __FUNCTION__, __LINE__);
 
 /**
@@ -97,8 +98,15 @@ class Logger {
 		std::atomic<bool>	m_runWorker;
 		std::thread 		*m_workerThread;
 
+		void log(int sysLogLvl, const char * lvlName, LogLevel appLogLvl, const std::string& msg, va_list args);
+		void sendToUdpSink(const std::string& msg);
 		void executeInterceptor(LogLevel level, const std::string& message);
 		void workerThread();
+		int m_UdpSockFD = -1;
+		struct sockaddr_in m_UdpServerAddr;
+		bool m_SyslogUdpEnabled = false;
+		std::string m_identifier;
+		std::string m_hostname;
 };
 
 #endif
diff --git a/C/common/logger.cpp b/C/common/logger.cpp
index 232877ceb5..690c07d744 100755
--- a/C/common/logger.cpp
+++ b/C/common/logger.cpp
@@ -15,13 +15,18 @@
 #include <memory>
 #include <string.h>
 #include <sys/time.h>
+#include <sys/socket.h>
 #include <exception>
+#include <arpa/inet.h>
 #include <stdexcept>
+#include <algorithm>
 
 using namespace std;
 
 // uncomment line below to get uSec level timestamps
 // #define ADD_USEC_TS
+const char * DEFALUT_LOG_IP = "127.0.0.1";
+const int DEFAULT_LOG_PORT = 5140;
 
 inline long getCurrTimeUsec()
 {
@@ -42,7 +47,7 @@ Logger *Logger::instance = 0;
  */
 Logger::Logger(const string& application) : m_runWorker(true), m_workerThread(NULL)
 {
-static char ident[80];
+	static char ident[80];
 
 	if (instance)
 	{
@@ -59,9 +64,72 @@ static char ident[80];
 	{
 		strncpy(ident, application.c_str(), sizeof(ident));
 	}
-	openlog(ident, LOG_PID|LOG_CONS, LOG_USER);
+
+	// Check if SYSLOG_UDP_ENABLED is set via environment variable
+	const char* udpEnabledEnv = std::getenv("SYSLOG_UDP_ENABLED");
+	m_SyslogUdpEnabled = false;
+
+	auto toLower = [](const std::string& s) {
+        std::string out = s;
+        std::transform(out.begin(), out.end(), out.begin(),
+            [](unsigned char c) { return std::tolower(c); });
+        return out;
+    };
+
+	if (udpEnabledEnv != nullptr && toLower(std::string(udpEnabledEnv)) == "true") 
+	{
+		m_SyslogUdpEnabled = true;
+	}
+
+	if(m_SyslogUdpEnabled)
+	{
+		// Check LOG_IP and LOG_PORT from environment variables with default values
+		const char* logIpEnv = std::getenv("LOG_IP");
+		const char* logPortEnv = std::getenv("LOG_PORT");
+
+		std::string logIp = logIpEnv ? logIpEnv : DEFALUT_LOG_IP; // Default to 127.0.0.1
+		int logPort = logPortEnv ? std::atoi(logPortEnv) : DEFAULT_LOG_PORT; // Default to port 5140
+		// Initialize the UDP socket
+		m_UdpSockFD = socket(AF_INET, SOCK_DGRAM, 0);
+		if (m_UdpSockFD >= 0) 
+		{
+			memset(&m_UdpServerAddr, 0, sizeof(m_UdpServerAddr));
+			m_UdpServerAddr.sin_family = AF_INET;
+			m_UdpServerAddr.sin_port = htons(logPort); // Use the port from LOG_PORT or default
+			if (inet_pton(AF_INET, logIp.c_str(), &m_UdpServerAddr.sin_addr) <= 0)
+			{
+				throw std::runtime_error("Invalid LOG_IP address");
+			}
+		} 
+		else 
+		{
+			throw std::runtime_error("Failed to create UDP socket");
+		}
+
+		if(m_SyslogUdpEnabled)
+		{
+			char hostname[256]; // Buffer to store the hostname
+	
+			// Retrieve the hostname (localhost name)
+			if (gethostname(hostname, sizeof(hostname)) != 0)
+			{
+				// Fallback in case of failure to retrieve hostname
+				strncpy(hostname, "localhost", sizeof(hostname) - 1);
+				hostname[sizeof(hostname) - 1] = '\0';
+			}
+			
+			m_hostname = hostname;
+		}
+	}
+	else
+	{
+		// Warning: these flags should be updated with caution because the `m_identifier` used in UDP when `m_SyslogUdpEnabled` is `true` may break
+		openlog(ident, LOG_PID|LOG_CONS, LOG_USER);
+	}
+
 	instance = this;
 	m_level = LOG_WARNING;
+	m_identifier = ident;
 }
 
 /**
@@ -83,7 +151,31 @@ Logger::~Logger()
 		m_workerThread = NULL;
 	}
 
-	closelog();
+	if(!m_SyslogUdpEnabled)
+	{
+		closelog();
+	}
+	else
+	{
+		if (m_UdpSockFD >= 0) 
+		{
+			close(m_UdpSockFD);
+			m_UdpSockFD = -1;
+		}
+	}
+}
+
+/**
+ * Send a message to the UDP sink if enabled
+ *
+ * @param msg		The message to send
+ */
+void Logger::sendToUdpSink(const std::string& msg) 
+{
+	if (m_UdpSockFD >= 0) 
+	{
+		sendto(m_UdpSockFD, msg.c_str(), msg.size(), 0, (struct sockaddr*)&m_UdpServerAddr, sizeof(m_UdpServerAddr));
+	}
 }
 
 /**
@@ -253,19 +345,12 @@ void Logger::workerThread()
  */
 void Logger::debug(const string& msg, ...)
 {
-	if (m_level == LOG_ERR || m_level == LOG_WARNING || m_level == LOG_INFO)
-	{
-		return;
-	}
 	va_list args;
 	va_start(args, msg);
-	string *fmt = format(msg, args);
-	syslog(LOG_DEBUG, "DEBUG: %s", fmt->c_str());
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::DEBUG, fmt->c_str());
-	}
-	delete fmt;
+
+	// Use the unified log function with the "DEBUG" level
+	log(LOG_DEBUG, "DEBUG", LogLevel::DEBUG, msg, args);
+
 	va_end(args);
 }
 
@@ -319,36 +404,23 @@ void Logger::printLongString(const string& s, LogLevel level)
 	}
 }
 
-
 /**
  * Log a message at the level info
  *
  * @param msg		A printf format string
  * @param ...		The variable arguments required by the printf format
  */
-void Logger::info(const string& msg, ...)
+void Logger::info(const std::string& msg, ...)
 {
-	if (m_level == LOG_ERR || m_level == LOG_WARNING)
-	{
-		return;
-	}
 	va_list args;
 	va_start(args, msg);
-	string *fmt = format(msg, args);
-#ifdef ADD_USEC_TS
-	syslog(LOG_INFO, "[.%06ld] INFO: %s", getCurrTimeUsec(), fmt->c_str());
-#else
-	syslog(LOG_INFO, "INFO: %s", fmt->c_str());
-#endif
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::INFO, fmt->c_str());
-	}
-	delete fmt;
+
+	// Use the unified log function with the "INFO" level
+	log(LOG_INFO, "INFO", LogLevel::INFO, msg, args);
+
 	va_end(args);
 }
 
-
 /**
  * Log a message at the level warn
  *
@@ -359,17 +431,13 @@ void Logger::warn(const string& msg, ...)
 {
 	va_list args;
 	va_start(args, msg);
-	string *fmt = format(msg, args);
-	syslog(LOG_WARNING, "WARNING: %s", fmt->c_str());
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::WARNING, fmt->c_str());
-	}
-	delete fmt;
+
+	// Use the unified log function with the "WARNING" level
+	log(LOG_WARNING, "WARNING", LogLevel::WARNING, msg, args);
+
 	va_end(args);
 }
 
-
 /**
  * Log a message at the level error
  *
@@ -380,17 +448,10 @@ void Logger::error(const string& msg, ...)
 {
 	va_list args;
 	va_start(args, msg);
-	string *fmt = format(msg, args);
-#ifdef ADD_USEC_TS
-		syslog(LOG_ERR, "[.%06ld] ERROR: %s", getCurrTimeUsec(), fmt->c_str());
-#else
-		syslog(LOG_ERR, "ERROR: %s", fmt->c_str());
-#endif
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::ERROR, fmt->c_str());
-	}
-	delete fmt;
+
+	// Use the unified log function with the "ERROR" level
+	log(LOG_ERR, "ERROR", LogLevel::ERROR, msg, args);
+
 	va_end(args);
 }
 
@@ -405,28 +466,63 @@ void Logger::fatal(const string& msg, ...)
 {
 	va_list args;
 	va_start(args, msg);
-	string *fmt = format(msg, args);
-	syslog(LOG_CRIT, "FATAL: %s", fmt->c_str());
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::FATAL, fmt->c_str());
-	}
-	delete fmt;
+
+	// Use the unified log function with the "FATAL" level
+	log(LOG_CRIT, "FATAL", LogLevel::FATAL, msg, args);
+
 	va_end(args);
 }
 
 /**
- * Apply the formatting to the error message
- * 
- * @param format	The printf format string
- * @param args		The printf argument list
- * @return string	The formatted string
+ * Log a message at the specified level
+ *
+ * @param sysLogLvl	The syslog level to use
+ * @param lvlName		The name of the log level
+ * @param appLogLvl	The application log level
+ * @param msg		A printf format string
+ * @param ...		The variable arguments required by the printf format
  */
-string *Logger::format( const std::string& format, va_list args)
+void Logger::log(int sysLogLvl, const char * lvlName, LogLevel appLogLvl, const std::string& msg, va_list args)
 {
-char	buf[1000];
+	// Check if the current log level allows messages
+	if (m_level < sysLogLvl) 
+	{
+		return;
+	}
 
-	  vsnprintf(buf, sizeof(buf), format.c_str(), args);
-	  return new string(buf);
-}
+	constexpr size_t MAX_BUFFER_SIZE = 1024; // Maximum allowed log size
+	char buffer[MAX_BUFFER_SIZE]; // Stack-allocated buffer for formatting
+
+	int copied = 0;
 
+	if(m_SyslogUdpEnabled)
+	{
+		// Add the identifier to the message in case udp
+		copied = snprintf(buffer, sizeof(buffer), "%s %s[%d]: ", m_hostname.c_str(), m_identifier.c_str(), getpid());
+	}
+
+#ifdef ADD_USEC_TS
+	copied += snprintf(buffer + copied, sizeof(buffer) - copied, "[.%06ld] %s: ", getCurrTimeUsec(), lvlName);
+#else
+	copied += snprintf(buffer + copied, sizeof(buffer) - copied, "%s: ", lvlName);
+#endif
+
+	// Format the log message using vsnprintf
+	vsnprintf(buffer + copied, sizeof(buffer) - copied, msg.c_str(), args);
+
+	if(m_SyslogUdpEnabled)
+	{
+		// Send the message to the UDP sink
+		sendToUdpSink(buffer);
+	}
+	else
+	{
+		syslog(sysLogLvl, "%s", buffer);
+	}
+
+	// Execute interceptors if any are present
+	if (!m_interceptors.empty())
+	{
+		executeInterceptor(appLogLvl, buffer);
+	}
+}
diff --git a/python/fledge/common/logger.py b/python/fledge/common/logger.py
index 8629058c50..f250284825 100644
--- a/python/fledge/common/logger.py
+++ b/python/fledge/common/logger.py
@@ -11,7 +11,7 @@
 import traceback
 from logging.handlers import SysLogHandler
 from functools import wraps
-
+import socket
 
 __author__ = "Praveen Garg, Ashish Jabble"
 __copyright__ = "Copyright (c) 2017-2023 OSIsoft, LLC"
@@ -31,6 +31,38 @@
 default_destination = SYSLOG
 """Default destination of logger"""
 
+log_udp = os.getenv('SYSLOG_UDP_ENABLED', 'False')
+
+def get_format() -> str:
+    """Set the format for the logger
+
+    Args:
+        fmt: format string
+    """
+        # TODO: Consider using %r with message when using syslog .. \n looks better than #
+    if log_udp.lower() == 'false':
+        fmt = '{}[%(process)d] %(levelname)s: %(module)s: %(name)s: %(message)s'.format(get_process_name())
+    else:
+        fmt = '{} {}[%(process)d] %(levelname)s: %(module)s: %(name)s: %(message)s'.format(socket.gethostname(), get_process_name())
+    return fmt
+
+def get_syslog_handler():
+    """Defines a syslog handler
+
+    Returns:
+         logging handler object : the syslog handler
+    """
+    # Retrieve LOG_PORT and LOG_IP from environment variables
+    if log_udp.lower() == 'true':
+        log_port = os.getenv('SYSLOG_PORT', '5140')
+        log_ip = os.getenv('SYSLOG_IP', '127.0.0.1') 
+        syslog_address = (log_ip, int(log_port))
+        syslog_handler = SysLogHandler(address=syslog_address)
+    else:
+        syslog_handler = SysLogHandler(address='/dev/log')
+
+    return syslog_handler
+
 
 def set_default_destination(destination: int):
     """ set_default_destination - allow a global default to be set, once, for all fledge modules
@@ -102,15 +134,13 @@ def setup(logger_name: str = None,
         destination = default_destination
 
     if destination == SYSLOG:
-        handler = SysLogHandler(address='/dev/log')
+        handler = get_syslog_handler()
     elif destination == CONSOLE:
         handler = logging.StreamHandler()  # stderr
     else:
         raise ValueError("Invalid destination {}".format(destination))
 
-    # TODO: Consider using %r with message when using syslog .. \n looks better than #
-    fmt = '{}[%(process)d] %(levelname)s: %(module)s: %(name)s: %(message)s'.format(get_process_name())
-    formatter = logging.Formatter(fmt=fmt)
+    formatter = logging.Formatter(fmt=get_format())
     handler.setFormatter(formatter)
     if level is not None:
         logger.setLevel(level)
@@ -177,9 +207,7 @@ class FLCoreLogger:
     def __new__(cls):
         if cls._instance is None:
             cls._instance = super().__new__(cls)
-            process_name = get_process_name()
-            fmt = '{}[%(process)d] %(levelname)s: %(module)s: %(name)s: %(message)s'.format(process_name)
-            cls.formatter = logging.Formatter(fmt=fmt)
+            cls.formatter = logging.Formatter(fmt=get_format())
         return cls._instance
 
     def get_syslog_handler(self):
@@ -188,7 +216,7 @@ def get_syslog_handler(self):
         Returns:
              logging handler object : the syslog handler
         """
-        syslog_handler = SysLogHandler(address='/dev/log')
+        syslog_handler = get_syslog_handler()
         syslog_handler.setFormatter(self.formatter)
         syslog_handler.name = "syslogHandler"
         return syslog_handler
diff --git a/scripts/common/write_log.sh b/scripts/common/write_log.sh
index d0973a322b..1d7661880a 100755
--- a/scripts/common/write_log.sh
+++ b/scripts/common/write_log.sh
@@ -23,7 +23,10 @@
 
 #set -x
 
-
+# Fetch environment variables for remote syslog configuration
+SYSLOG_UDP_ENABLED=${SYSLOG_UDP_ENABLED:-false} # Enable/disable remote syslog (default: false)
+LOG_IP=${LOG_IP:-"127.0.0.1"}                   # Remote syslog server IP (default: localhost)
+LOG_PORT=${LOG_PORT:-5140}                       # Remote syslog server port (default: 514)
 ## The Loggging Handler
 #
 # Paramaters: $1 - Module
@@ -78,7 +81,13 @@ write_log() {
   # Log to syslog
   if [[ "$5" =~ ^(logonly|all)$ ]]; then
       tag="Fledge ${1}[${BASHPID}] ${severity}: ${2}"
-      logger -t "${tag}" "${4}"
+      if [[ "${SYSLOG_UDP_ENABLED,,}" == "true" ]]; then
+          # Send log to remote syslog server using logger
+          logger --server "${LOG_IP}" --port "${LOG_PORT}" -t ${tag} "${4}"
+      else
+          # Log locally
+          logger -t "${tag}" "${4}"
+      fi
   fi
 
   # Log to Stdout


Reviews:
user: login: MarkRiddoch
body: It is probably the fault of your editing tool, but the code formatting of the C++ code is messed up. Please make sure you use something that shows tabs correctly and correct the formatting.


state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:51:15Z
user: login: ashwini-k-pandey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T07:27:39Z
user: login: ashwini-k-pandey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T09:02:30Z
user: login: MarkRiddoch
body: This gives us the short term workaround for logging with containers. The problem is it also means that the ability to view logs and create support bundles has been compromised. Longer term we will need to think about a better way to tackle this. However we should in terms of short term merge this for customer use.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:16:27Z
user: login: MarkRiddoch
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:43:10Z
user: login: MarkRiddoch
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:19:24Z
user: login: MarkRiddoch
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:39:13Z

Review comments:
diff_hunk: @@ -29,6 +33,20 @@ inline long getCurrTimeUsec()
 	return m_timestamp.tv_usec;
 }
 
+/**
+ * Compare two strings in a case insensitive manner
+ *
+ * @param str1		The first string
+ * @param str2		The second string
+ * @return bool		True if the strings are equal, false otherwise
+ */
+bool caseInsensitiveCompare(const std::string& str1, const std::string& str2) {
path: C/common/logger.cpp
user: login: MarkRiddoch
body: You don't use this, why is it here? It is not within the Logger class or belong with the Logger functionality. 
created_at: 2025-05-06T08:40:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,9 +76,53 @@ static char ident[80];
 	{
 		strncpy(ident, application.c_str(), sizeof(ident));
 	}
-	openlog(ident, LOG_PID|LOG_CONS, LOG_USER);
+
+	//openlog(ident, LOG_PID|LOG_CONS, LOG_USER);
path: C/common/logger.cpp
user: login: MarkRiddoch
body: Don't leave commented out code in the final checkin.
created_at: 2025-05-06T08:41:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -402,30 +452,55 @@ void Logger::error(const string& msg, ...)
  */
 void Logger::fatal(const string& msg, ...)
 {
-	va_list args;
-	va_start(args, msg);
-	string *fmt = format(msg, args);
-	syslog(LOG_CRIT, "FATAL: %s", fmt->c_str());
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::FATAL, fmt->c_str());
-	}
-	delete fmt;
-	va_end(args);
+    va_list args;
+    va_start(args, msg);
+
+    // Use the unified log function with the "FATAL" level
+    log(LOG_CRIT, "FATAL", msg, args);
+
+    va_end(args);
 }
 
-/**
- * Apply the formatting to the error message
- * 
- * @param format	The printf format string
- * @param args		The printf argument list
- * @return string	The formatted string
- */
-string *Logger::format( const std::string& format, va_list args)
+
+void Logger::log(int lvl, const char * lvlName, const string& msg, ...)
path: C/common/logger.cpp
user: login: MarkRiddoch
body: Please put back the doxygen header for the renamed method.
created_at: 2025-05-06T08:48:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,9 +76,53 @@ static char ident[80];
 	{
 		strncpy(ident, application.c_str(), sizeof(ident));
 	}
-	openlog(ident, LOG_PID|LOG_CONS, LOG_USER);
+
+	//openlog(ident, LOG_PID|LOG_CONS, LOG_USER);
path: C/common/logger.cpp
user: login: ashwini-k-pandey
body: Done
created_at: 2025-05-07T07:27:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -402,30 +452,55 @@ void Logger::error(const string& msg, ...)
  */
 void Logger::fatal(const string& msg, ...)
 {
-	va_list args;
-	va_start(args, msg);
-	string *fmt = format(msg, args);
-	syslog(LOG_CRIT, "FATAL: %s", fmt->c_str());
-	if (!m_interceptors.empty())
-	{
-		executeInterceptor(LogLevel::FATAL, fmt->c_str());
-	}
-	delete fmt;
-	va_end(args);
+    va_list args;
+    va_start(args, msg);
+
+    // Use the unified log function with the "FATAL" level
+    log(LOG_CRIT, "FATAL", msg, args);
+
+    va_end(args);
 }
 
-/**
- * Apply the formatting to the error message
- * 
- * @param format	The printf format string
- * @param args		The printf argument list
- * @return string	The formatted string
- */
-string *Logger::format( const std::string& format, va_list args)
+
+void Logger::log(int lvl, const char * lvlName, const string& msg, ...)
path: C/common/logger.cpp
user: login: ashwini-k-pandey
body: done
created_at: 2025-05-07T07:27:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,6 +33,20 @@ inline long getCurrTimeUsec()
 	return m_timestamp.tv_usec;
 }
 
+/**
+ * Compare two strings in a case insensitive manner
+ *
+ * @param str1		The first string
+ * @param str2		The second string
+ * @return bool		True if the strings are equal, false otherwise
+ */
+bool caseInsensitiveCompare(const std::string& str1, const std::string& str2) {
path: C/common/logger.cpp
user: login: ashwini-k-pandey
body: removed..
created_at: 2025-05-07T07:28:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: ashwini-k-pandey
created_at: 2025-05-08T14:07:56Z
body: @MarkRiddoch The current solution addressing the sending logs over UDP in a Docker container. I agrees that reading log from syslog has few limitations.  We should enhance the log reading, log writing, and log searching capabilities in FogLAMP holistically, several improvements can be proposed to create a more robust and efficient logging system.  If you agree I would raise a JIRA with my proposal on this
author_association: CONTRIBUTOR
user: login: ashwini-k-pandey
created_at: 2025-05-08T14:11:00Z
body: @MarkRiddoch From FogLAMP running in the docker container, we can see the Logs from GUI and create the support bundle, same as usual FogLAMP instance. There is no change in user experience with these changes.
author_association: CONTRIBUTOR