diff --git a/src/sonic-eventd/Makefile b/src/sonic-eventd/Makefile new file mode 100644 index 00000000000..b21689a76fa --- /dev/null +++ b/src/sonic-eventd/Makefile @@ -0,0 +1,83 @@ +RM := rm -rf +EVENTD_TARGET := eventd +EVENTD_TEST := tests/tests +EVENTD_TOOL := tools/events_tool +RSYSLOG-PLUGIN_TARGET := rsyslog_plugin +RSYSLOG-PLUGIN_TEST: rsyslog_plugin_tests/tests +CP := cp +MKDIR := mkdir +CC := g++ +MV := mv +LIBS := -levent -lhiredis -lswsscommon -lpthread -lboost_thread -lboost_system -lzmq -lboost_serialization -luuid +TEST_LIBS := -L/usr/src/gtest -lgtest -lgtest_main -lgmock -lgmock_main + +CFLAGS += -Wall -std=c++17 -fPIE -I$(PWD)/../sonic-swss-common/common +PWD := $(shell pwd) + +ifneq ($(MAKECMDGOALS),clean) +ifneq ($(strip $(C_DEPS)),) +-include $(C_DEPS) $(OBJS) +endif +endif + +-include src/subdir.mk +-include tests/subdir.mk +-include tools/subdir.mk +-include rsyslog_plugin/subdir.mk + +all: sonic-eventd eventd-tests eventd-tool rsyslog-plugin rsyslog-plugin-tests + +sonic-eventd: $(OBJS) + @echo 'Building target: $@' + @echo 'Invoking: G++ Linker' + $(CC) $(LDFLAGS) -o $(EVENTD_TARGET) $(OBJS) $(LIBS) + @echo 'Finished building target: $@' + @echo ' ' + +eventd-tool: $(TOOL_OBJS) + @echo 'Building target: $@' + @echo 'Invoking: G++ Linker' + $(CC) $(LDFLAGS) -o $(EVENTD_TOOL) $(TOOL_OBJS) $(LIBS) + @echo 'Finished building target: $@' + @echo ' ' + +rsyslog-plugin: $(RSYSLOG-PLUGIN_OBJS) + @echo 'Buidling Target: $@' + @echo 'Invoking: G++ Linker' + $(CC) $(LDFLAGS) -o $(RSYSLOG-PLUGIN_TARGET) $(RSYSLOG-PLUGIN_OBJS) $(LIBS) + @echo 'Finished building target: $@' + @echo ' ' + +eventd-tests: $(TEST_OBJS) + @echo 'Building target: $@' + @echo 'Invoking: G++ Linker' + $(CC) $(LDFLAGS) -o $(EVENTD_TEST) $(TEST_OBJS) $(LIBS) $(TEST_LIBS) + @echo 'Finished building target: $@' + $(EVENTD_TEST) + @echo 'Finished running tests' + @echo ' ' + +rsyslog-plugin-tests: $(RSYSLOG-PLUGIN-TEST_OBJS) + @echo 'BUILDING target: $@' + @echo 'Invoking G++ Linker' + $(CC) $(LDFLAGS) -o $(RSYSLOG-PLUGIN_TEST) $(RSYSLOG-PLUGIN-TEST_OBJS) $(LIBS) $(TEST_LIBS) + @echo 'Finished building target: $@' + $(RSYSLOG-PLUGIN_TEST) + @echo 'Finished running tests' + @echo ' ' + +install: + $(MKDIR) -p $(DESTDIR)/usr/sbin + $(MV) $(EVENTD_TARGET) $(DESTDIR)/usr/sbin + $(MV) $(EVENTD_TOOL) $(DESTDIR)/usr/sbin + $(MV) $(RSYSLOG-PLUGIN_TARGET) $(DESTDIR)/usr/sbin + +deinstall: + $(RM) $(DESTDIR)/usr/sbin/$(EVENTD_TARGET) + $(RM) $(DESTDIR)/usr/sbin/$(RSYSLOG-PLUGIN_TARGET) + $(RM) -rf $(DESTDIR)/usr/sbin + +clean: + -@echo ' ' + +.PHONY: all clean dependents diff --git a/src/sonic-eventd/rsyslog_plugin/main.cpp b/src/sonic-eventd/rsyslog_plugin/main.cpp new file mode 100644 index 00000000000..53162608c5a --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin/main.cpp @@ -0,0 +1,57 @@ +#include +#include +#include +#include "rsyslog_plugin.h" + +#define SUCCESS_CODE 0 +#define INVALID_REGEX_ERROR_CODE 1 +#define EVENT_INIT_PUBLISH_ERROR_CODE 2 +#define MISSING_ARGS_ERROR_CODE 3 + +void showUsage() { + cout << "Usage for rsyslog_plugin: \n" << "options\n" + << "\t-r,required,type=string\t\tPath to regex file\n" + << "\t-m,required,type=string\t\tYANG module name of source generating syslog message\n" + << "\t-h \t\tHelp" + << endl; +} + +int main(int argc, char** argv) { + string regexPath; + string moduleName; + int optionVal; + + while((optionVal = getopt(argc, argv, "r:m:h")) != -1) { + switch(optionVal) { + case 'r': + regexPath = optarg; + break; + case 'm': + moduleName = optarg; + break; + case 'h': + case '?': + default: + showUsage(); + return 1; + } + } + + if(regexPath.empty() || moduleName.empty()) { // Missing required rc path + cerr << "Error: Missing regexPath and moduleName." << endl; + return MISSING_ARGS_ERROR_CODE; + } + + unique_ptr plugin(new RsyslogPlugin(moduleName, regexPath)); + int returnCode = plugin->onInit(); + if(returnCode == INVALID_REGEX_ERROR_CODE) { + SWSS_LOG_ERROR("Rsyslog plugin was not able to be initialized due to invalid regex file provided.\n"); + return returnCode; + } else if(returnCode == EVENT_INIT_PUBLISH_ERROR_CODE) { + SWSS_LOG_ERROR("Rsyslog plugin was not able to be initialized due to event_init_publish call failing.\n"); + return returnCode; + } + + plugin->run(); + return SUCCESS_CODE; +} diff --git a/src/sonic-eventd/rsyslog_plugin/rsyslog_plugin.cpp b/src/sonic-eventd/rsyslog_plugin/rsyslog_plugin.cpp new file mode 100644 index 00000000000..ca50f465a79 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin/rsyslog_plugin.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include "rsyslog_plugin.h" +#include "common/json.hpp" + +using json = nlohmann::json; + +bool RsyslogPlugin::onMessage(string msg) { + string tag; + event_params_t paramDict; + if(!m_parser->parseMessage(msg, tag, paramDict)) { + SWSS_LOG_DEBUG("%s was not able to be parsed into a structured event\n", msg.c_str()); + return false; + } else { + int returnCode = event_publish(m_eventHandle, tag, ¶mDict); + if (returnCode != 0) { + SWSS_LOG_ERROR("rsyslog_plugin was not able to publish event for %s. last thrown event error: %d\n", tag.c_str(), event_last_error()); + return false; + } + return true; + } +} + +bool RsyslogPlugin::createRegexList() { + fstream regexFile; + regexFile.open(m_regexPath, ios::in); + if (!regexFile) { + SWSS_LOG_ERROR("No such path exists: %s for source %s\n", m_regexPath.c_str(), m_moduleName.c_str()); + return false; + } + try { + regexFile >> m_parser->m_regexList; + } catch (invalid_argument& iaException) { + SWSS_LOG_ERROR("Invalid JSON file: %s, throws exception: %s\n", m_regexPath.c_str(), iaException.what()); + return false; + } + + string regexString; + regex expression; + + for(long unsigned int i = 0; i < m_parser->m_regexList.size(); i++) { + try { + regexString = m_parser->m_regexList[i]["regex"]; + string tag = m_parser->m_regexList[i]["tag"]; + vector params = m_parser->m_regexList[i]["params"]; + regex expr(regexString); + expression = expr; + } catch (domain_error& deException) { + SWSS_LOG_ERROR("Missing required key, throws exception: %s\n", deException.what()); + return false; + } catch (regex_error& reException) { + SWSS_LOG_ERROR("Invalid regex, throws exception: %s\n", reException.what()); + return false; + } + m_parser->m_expressions.push_back(expression); + } + if(m_parser->m_expressions.empty()) { + SWSS_LOG_ERROR("Empty list of regex expressions.\n"); + return false; + } + regexFile.close(); + return true; +} + +[[noreturn]] void RsyslogPlugin::run() { + while(true) { + string line; + getline(cin, line); + if(line.empty()) { + continue; + } + onMessage(line); + } +} + +int RsyslogPlugin::onInit() { + m_eventHandle = events_init_publisher(m_moduleName); + bool success = createRegexList(); + if(!success) { + return 1; // invalid regex error code + } else if(m_eventHandle == NULL) { + return 2; // event init publish error code + } + return 0; +} + +RsyslogPlugin::RsyslogPlugin(string moduleName, string regexPath) { + m_parser = unique_ptr(new SyslogParser()); + m_moduleName = moduleName; + m_regexPath = regexPath; +} diff --git a/src/sonic-eventd/rsyslog_plugin/rsyslog_plugin.h b/src/sonic-eventd/rsyslog_plugin/rsyslog_plugin.h new file mode 100644 index 00000000000..e5c5b13fca7 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin/rsyslog_plugin.h @@ -0,0 +1,34 @@ +#ifndef RSYSLOG_PLUGIN_H +#define RSYSLOG_PLUGIN_H + +#include +#include +#include "syslog_parser.h" +#include "common/events.h" +#include "common/logger.h" + +using namespace std; +using namespace swss; + +/** + * Rsyslog Plugin will utilize an instance of a syslog parser to read syslog messages from rsyslog.d and will continuously read from stdin + * A plugin instance is created for each container/host. + * + */ + +class RsyslogPlugin { +public: + int onInit(); + bool onMessage(string msg); + void run(); + RsyslogPlugin(string moduleName, string regexPath); +private: + unique_ptr m_parser; + event_handle_t m_eventHandle; + string m_regexPath; + string m_moduleName; + bool createRegexList(); +}; + +#endif + diff --git a/src/sonic-eventd/rsyslog_plugin/subdir.mk b/src/sonic-eventd/rsyslog_plugin/subdir.mk new file mode 100644 index 00000000000..e5af1085da4 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin/subdir.mk @@ -0,0 +1,13 @@ +CC := g++ + +RSYSLOG-PLUGIN_TEST_OBJS += ./rsyslog_plugin/rsyslog_plugin.o ./rsyslog_plugin/syslog_parser.o +RSYSLOG-PLUGIN_OBJS += ./rsyslog_plugin/rsyslog_plugin.o ./rsyslog_plugin/syslog_parser.o ./rsyslog_plugin/main.o + +C_DEPS += ./rsyslog_plugin/rsyslog_plugin.d ./rsyslog_plugin/syslog_parser.d ./rsyslog_plugin/main.d + +rsyslog_plugin/%.o: rsyslog_plugin/%.cpp + @echo 'Building file: $<' + @echo 'Invoking: GCC C++ Compiler' + $(CC) -D__FILENAME__="$(subst rsyslog_plugin/,,$<)" $(CFLAGS) -c -fmessage-length=0 -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@)" -o "$(@)" "$<" + @echo 'Finished building: $< + '@echo ' ' diff --git a/src/sonic-eventd/rsyslog_plugin/syslog_parser.cpp b/src/sonic-eventd/rsyslog_plugin/syslog_parser.cpp new file mode 100644 index 00000000000..a025cc02953 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin/syslog_parser.cpp @@ -0,0 +1,28 @@ +#include +#include "syslog_parser.h" +#include "common/logger.h" + +/** + * Parses syslog message and returns structured event + * + * @param nessage us syslog message being fed in by rsyslog.d + * @return return structured event json for publishing + * +*/ + +bool SyslogParser::parseMessage(string message, string& eventTag, event_params_t& paramMap) { + for(long unsigned int i = 0; i < m_regexList.size(); i++) { + smatch matchResults; + vector params = m_regexList[i]["params"]; + if(!regex_search(message, matchResults, m_expressions[i]) || params.size() != matchResults.size() - 1) { + continue; + } + // found matching regex + eventTag = m_regexList[i]["tag"]; + transform(params.begin(), params.end(), matchResults.begin() + 1, inserter(paramMap, paramMap.end()), [](string a, string b) { + return make_pair(a,b); + }); + return true; + } + return false; +} diff --git a/src/sonic-eventd/rsyslog_plugin/syslog_parser.h b/src/sonic-eventd/rsyslog_plugin/syslog_parser.h new file mode 100644 index 00000000000..53128f1edf9 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin/syslog_parser.h @@ -0,0 +1,26 @@ +#ifndef SYSLOG_PARSER_H +#define SYSLOG_PARSER_H + +#include +#include +#include +#include "common/json.hpp" +#include "common/events.h" + +using namespace std; +using json = nlohmann::json; + +/** + * Syslog Parser is responsible for parsing log messages fed by rsyslog.d and returns + * matched result to rsyslog_plugin to use with events publish API + * + */ + +class SyslogParser { +public: + vector m_expressions; + json m_regexList = json::array(); + bool parseMessage(string message, string& tag, event_params_t& paramDict); +}; + +#endif diff --git a/src/sonic-eventd/rsyslog_plugin_tests/rsyslog_plugin_ut.cpp b/src/sonic-eventd/rsyslog_plugin_tests/rsyslog_plugin_ut.cpp new file mode 100644 index 00000000000..5893c70f66c --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/rsyslog_plugin_ut.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include "gtest/gtest.h" +#include "common/json.hpp" +#include "common/events.h" +#include "rsyslog_plugin/rsyslog_plugin.h" +#include "rsyslog_plugin/syslog_parser.h" + +using namespace std; +using json = nlohmann::json; + +TEST(syslog_parser, matching_regex) { + json jList = json::array(); + vector testExpressions; + string regexString = "timestamp (.*) message (.*) other_data (.*)"; + json jTest; + jTest["tag"] = "test_tag"; + jTest["regex"] = regexString; + jTest["params"] = { "timestamp", "message", "other_data" }; + jList.push_back(jTest); + regex expression(regexString); + testExpressions.push_back(expression); + + string tag; + event_params_t paramDict; + + event_params_t expectedDict; + expectedDict["timestamp"] = "test_timestamp"; + expectedDict["message"] = "test_message"; + expectedDict["other_data"] = "test_data"; + + SyslogParser* parser = new SyslogParser(); + parser->m_expressions = testExpressions; + parser->m_regexList = jList; + + bool success = parser->parseMessage("timestamp test_timestamp message test_message other_data test_data", tag, paramDict); + EXPECT_EQ(true, success); + EXPECT_EQ("test_tag", tag); + EXPECT_EQ(expectedDict, paramDict); + + delete parser; +} + +TEST(syslog_parser, no_matching_regex) { + json jList = json::array(); + vector testExpressions; + string regexString = "no match"; + json jTest; + jTest["tag"] = "test_tag"; + jTest["regex"] = regexString; + jTest["params"] = vector(); + jList.push_back(jTest); + regex expression(regexString); + testExpressions.push_back(expression); + + string tag; + event_params_t paramDict; + + SyslogParser* parser = new SyslogParser(); + parser->m_expressions = testExpressions; + parser->m_regexList = jList; + + bool success = parser->parseMessage("Test Message", tag, paramDict); + EXPECT_EQ(false, success); + delete parser; +} + + +RsyslogPlugin* createPlugin(string path) { + RsyslogPlugin* plugin = new RsyslogPlugin("test_mod_name", path); + return plugin; +} + +TEST(rsyslog_plugin, onInit_invalidJS0N) { + auto plugin = createPlugin("./test_regex_1.rc.json"); + EXPECT_NE(0, plugin->onInit()); + delete plugin; +} + +TEST(rsyslog_plugin, onInit_emptyJSON) { + auto plugin = createPlugin("./test_regex_6.rc.json"); + EXPECT_NE(0, plugin->onInit()); + delete plugin; +} + +TEST(rsyslog_plugin, onInit_missingRegex) { + auto plugin = createPlugin("./test_regex_3.rc.json"); + EXPECT_NE(0, plugin->onInit()); + delete plugin; +} + +TEST(rsyslog_plugin, onInit_invalidRegex) { + auto plugin = createPlugin("./test_regex_4.rc.json"); + EXPECT_NE(0, plugin->onInit()); + delete plugin; +} + +TEST(rsyslog_plugin, onMessage) { + auto plugin = createPlugin("./test_regex_2.rc.json"); + EXPECT_EQ(0, plugin->onInit()); + + ifstream infile("test_syslogs.txt"); + string logMessage; + bool parseResult; + while(infile >> logMessage >> parseResult) { + EXPECT_EQ(parseResult, plugin->onMessage(logMessage)); + } + infile.close(); + delete plugin; +} + +TEST(rsyslog_plugin, onMessage_noParams) { + auto plugin = createPlugin("./test_regex_5.rc.json"); + EXPECT_EQ(0, plugin->onInit()); + + ifstream infile("test_syslogs_2.txt"); + string logMessage; + bool parseResult; + while(infile >> logMessage >> parseResult) { + EXPECT_EQ(parseResult, plugin->onMessage(logMessage)); + } + infile.close(); + delete plugin; +} + +int main(int argc, char* argv[]) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/sonic-eventd/rsyslog_plugin_tests/subdir.mk b/src/sonic-eventd/rsyslog_plugin_tests/subdir.mk new file mode 100644 index 00000000000..dd2e47364a2 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/subdir.mk @@ -0,0 +1,12 @@ +CC := g++ + +RSYSLOG-PLUGIN_TEST_OBJS += ./rsyslog_plugin_tests/rsyslog_plugin_ut.o + +C_DEPS += ./rsyslog_plugin_tests/rsyslog_plugin_ut.d + +rsyslog_plugin_tests/%.o: rsyslog_plugin_tests/%.cpp + @echo 'Building file: $<' + @echo 'Invoking: GCC C++ Compiler' + $(CC) -D__FILENAME__="$(subst rsyslog_plugin_tests/,,$<)" $(CFLAGS) -c -fmessage-length=0 -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@)" -o "$(@)" "$<" + @echo 'Finished building: $< + '@echo ' ' diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_regex_1.rc.json b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_1.rc.json new file mode 100644 index 00000000000..72e8ffc0db8 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_1.rc.json @@ -0,0 +1 @@ +* diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_regex_2.rc.json b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_2.rc.json new file mode 100644 index 00000000000..5c9a2e9612e --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_2.rc.json @@ -0,0 +1,7 @@ +[ + { + "tag": "bgp-state", + "regex": "([a-zA-Z]{3} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{0,6}) .* %ADJCHANGE: neighbor (.*) (Up|Down) .*", + "params": [ "timestamp", "neighbor_ip", "state" ] + } +] diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_regex_3.rc.json b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_3.rc.json new file mode 100644 index 00000000000..2e67e88f844 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_3.rc.json @@ -0,0 +1,6 @@ +[ + { + "tag": "TEST-TAG-NO-REGEX", + "param": [] + } +] diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_regex_4.rc.json b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_4.rc.json new file mode 100644 index 00000000000..244b601fbac --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_4.rc.json @@ -0,0 +1,7 @@ +[ + { + "tag": "TEST-TAG-INVALID-REGEX", + "regex": "++", + "params": [] + } +] diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_regex_5.rc.json b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_5.rc.json new file mode 100644 index 00000000000..ddaf37c931a --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_5.rc.json @@ -0,0 +1,7 @@ +[ + { + "tag": "test_tag", + "regex": ".*", + "params": [] + } +] diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_regex_6.rc.json b/src/sonic-eventd/rsyslog_plugin_tests/test_regex_6.rc.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_syslogs.txt b/src/sonic-eventd/rsyslog_plugin_tests/test_syslogs.txt new file mode 100644 index 00000000000..78f89aec3d2 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_syslogs.txt @@ -0,0 +1,4 @@ +"Aug 17 02:39:21.286611 SN6-0101-0114-02T0 INFO bgp#bgpd[62]: %ADJCHANGE: neighbor 100.126.188.90 Down Neighbor deleted" true +"Aug 17 02:46:42.615668 SN6-0101-0114-02T0 INFO bgp#bgpd[62]: %ADJCHANGE: neighbor 100.126.188.90 Up" true +"Aug 17 04:46:51.290979 SN6-0101-0114-02T0 INFO bgp#bgpd[62]: %ADJCHANGE: neighbor 100.126.188.78 Down Neighbor deleted" true +"Aug 17 04:46:51.290979 SN6-0101-0114-02T0 INFO bgp#bgpd[62]: %NOEVENT: no event" false diff --git a/src/sonic-eventd/rsyslog_plugin_tests/test_syslogs_2.txt b/src/sonic-eventd/rsyslog_plugin_tests/test_syslogs_2.txt new file mode 100644 index 00000000000..d56615f6168 --- /dev/null +++ b/src/sonic-eventd/rsyslog_plugin_tests/test_syslogs_2.txt @@ -0,0 +1,3 @@ +testMessage true +another_test_message true + true