new cobra to python bot (still sending to statsd)
values + string building can be done in python (we are embedding it)
This commit is contained in:
		| @@ -10,6 +10,7 @@ set (IXBOTS_SOURCES | ||||
|     ixbots/IXCobraToStdoutBot.cpp | ||||
|     ixbots/IXCobraMetricsToStatsdBot.cpp | ||||
|     ixbots/IXCobraMetricsToRedisBot.cpp | ||||
|     ixbots/IXCobraToPythonBot.cpp | ||||
|     ixbots/IXStatsdClient.cpp | ||||
| ) | ||||
|  | ||||
| @@ -21,6 +22,7 @@ set (IXBOTS_HEADERS | ||||
|     ixbots/IXCobraToStdoutBot.h | ||||
|     ixbots/IXCobraMetricsToStatsdBot.h | ||||
|     ixbots/IXCobraMetricsToRedisBot.h | ||||
|     ixbots/IXCobraToPythonBot.h | ||||
|     ixbots/IXStatsdClient.h | ||||
| ) | ||||
|  | ||||
| @@ -34,6 +36,8 @@ if (NOT JSONCPP_FOUND) | ||||
|   set(JSONCPP_INCLUDE_DIRS ../third_party/jsoncpp) | ||||
| endif() | ||||
|  | ||||
| find_package(Python COMPONENTS Development) | ||||
|  | ||||
| set(IXBOTS_INCLUDE_DIRS | ||||
|     . | ||||
|     .. | ||||
| @@ -43,6 +47,7 @@ set(IXBOTS_INCLUDE_DIRS | ||||
|     ../ixredis | ||||
|     ../ixsentry | ||||
|     ${JSONCPP_INCLUDE_DIRS} | ||||
|     ${SPDLOG_INCLUDE_DIRS}) | ||||
|     ${SPDLOG_INCLUDE_DIRS} | ||||
|     ${Python_INCLUDE_DIRS}) | ||||
|  | ||||
| target_include_directories( ixbots PUBLIC ${IXBOTS_INCLUDE_DIRS} ) | ||||
|   | ||||
							
								
								
									
										343
									
								
								ixbots/ixbots/IXCobraToPythonBot.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								ixbots/ixbots/IXCobraToPythonBot.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,343 @@ | ||||
| /* | ||||
|  *  IXCobraToPythonBot.cpp | ||||
|  *  Author: Benjamin Sergeant | ||||
|  *  Copyright (c) 2020 Machine Zone, Inc. All rights reserved. | ||||
|  */ | ||||
|  | ||||
| #include "IXCobraToPythonBot.h" | ||||
|  | ||||
| #include "IXCobraBot.h" | ||||
| #include "IXStatsdClient.h" | ||||
| #include <chrono> | ||||
| #include <ixcobra/IXCobraConnection.h> | ||||
| #include <ixcore/utils/IXCoreLogger.h> | ||||
| #include <sstream> | ||||
| #include <vector> | ||||
| #include <algorithm> | ||||
| #include <map> | ||||
| #include <cctype> | ||||
|  | ||||
| // | ||||
| // I cannot get Windows to easily build on CI (github action) so support | ||||
| // is disabled for now. It should be a simple fix  | ||||
| // (linking error about missing debug build) | ||||
| // | ||||
|  | ||||
| #ifndef _WIN32 | ||||
| #define PY_SSIZE_T_CLEAN | ||||
| #include <Python.h> | ||||
| #endif | ||||
|  | ||||
| #ifndef _WIN32 | ||||
| namespace | ||||
| { | ||||
|     // | ||||
|     // This function is unused at this point. It produce a correct output, | ||||
|     // but triggers memory leaks when called repeateadly, as the reference counting | ||||
|     // Python functions are not used properly | ||||
|     // | ||||
|     PyObject* jsonToPythonObject(const Json::Value& val) | ||||
|     { | ||||
|         switch(val.type()) | ||||
|         { | ||||
|             case Json::nullValue: | ||||
|             { | ||||
|                 return Py_None; | ||||
|             } | ||||
|  | ||||
|             case Json::intValue: | ||||
|             { | ||||
|                 return PyLong_FromLong(val.asInt64()); | ||||
|             } | ||||
|  | ||||
|             case Json::uintValue: | ||||
|             { | ||||
|                 return PyLong_FromLong(val.asUInt64()); | ||||
|             } | ||||
|  | ||||
|             case Json::realValue: | ||||
|             { | ||||
|                 return PyFloat_FromDouble(val.asDouble()); | ||||
|             } | ||||
|  | ||||
|             case Json::stringValue: | ||||
|             { | ||||
|                 return PyUnicode_FromString(val.asCString()); | ||||
|             } | ||||
|  | ||||
|             case Json::booleanValue: | ||||
|             { | ||||
|                 return val.asBool() ? Py_True : Py_False; | ||||
|             } | ||||
|  | ||||
|             case Json::arrayValue: | ||||
|             { | ||||
|                 PyObject* list = PyList_New(val.size()); | ||||
|                 Py_ssize_t i = 0; | ||||
|                 for (auto&& it = val.begin(); it != val.end(); ++it) | ||||
|                 { | ||||
|                     PyList_SetItem(list, i++, jsonToPythonObject(*it)); | ||||
|                 } | ||||
|                 return list; | ||||
|             } | ||||
|  | ||||
|             case Json::objectValue: | ||||
|             { | ||||
|                 PyObject* dict = PyDict_New(); | ||||
|                 for (auto&& it = val.begin(); it != val.end(); ++it) | ||||
|                 { | ||||
|                     PyObject* key = jsonToPythonObject(it.key()); | ||||
|                     PyObject* value = jsonToPythonObject(*it); | ||||
|  | ||||
|                     PyDict_SetItem(dict, key, value); | ||||
|                 } | ||||
|                 return dict; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| namespace ix | ||||
| { | ||||
|     int64_t cobra_to_python_bot(const ix::CobraBotConfig& config, | ||||
|                                 StatsdClient& statsdClient, | ||||
|                                 const std::string& scriptPath) | ||||
|     { | ||||
| #ifdef _WIN32 | ||||
|         CoreLogger::error("Command is disabled on Windows."); | ||||
|         return -1; | ||||
| #else | ||||
|         CobraBot bot; | ||||
|         Py_InitializeEx(0); // 0 arg so that we do not install signal handlers  | ||||
|                             // which prevent us from using Ctrl-C | ||||
|  | ||||
|         size_t lastIndex = scriptPath.find_last_of(".");  | ||||
|         std::string modulePath = scriptPath.substr(0, lastIndex); | ||||
|  | ||||
|         PyObject* pyModuleName = PyUnicode_DecodeFSDefault(modulePath.c_str()); | ||||
|  | ||||
|         if (pyModuleName == nullptr) | ||||
|         { | ||||
|             CoreLogger::error("Python error: Cannot decode file system path"); | ||||
|             PyErr_Print(); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Import module | ||||
|         PyObject* pyModule = PyImport_Import(pyModuleName); | ||||
|         Py_DECREF(pyModuleName); | ||||
|         if (pyModule == nullptr) | ||||
|         { | ||||
|             CoreLogger::error("Python error: Cannot import module."); | ||||
|             CoreLogger::error("Module name cannot countain dash characters."); | ||||
|             CoreLogger::error("Is PYTHONPATH set correctly ?"); | ||||
|             PyErr_Print(); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // module main funtion name is named 'run' | ||||
|         const std::string entryPoint("run"); | ||||
|         PyObject* pyFunc = PyObject_GetAttrString(pyModule, entryPoint.c_str()); | ||||
|  | ||||
|         if (!pyFunc) | ||||
|         { | ||||
|             CoreLogger::error("run symbol is missing from module."); | ||||
|             PyErr_Print(); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (!PyCallable_Check(pyFunc)) | ||||
|         { | ||||
|             CoreLogger::error("run symbol is not a function."); | ||||
|             PyErr_Print(); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         bot.setOnBotMessageCallback( | ||||
|             [&statsdClient, pyFunc] | ||||
|                 (const Json::Value& msg, | ||||
|                  const std::string& /*position*/, | ||||
|                  std::atomic<bool>& /*throttled*/, | ||||
|                  std::atomic<bool>& fatalCobraError, | ||||
|                  std::atomic<uint64_t>& sentCount) -> void { | ||||
|             if (msg["device"].isNull()) | ||||
|             { | ||||
|                 CoreLogger::info("no device entry, skipping event"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (msg["id"].isNull()) | ||||
|             { | ||||
|                 CoreLogger::info("no id entry, skipping event"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // | ||||
|             // Invoke python script here. First build function parameters, a tuple | ||||
|             // | ||||
|             const int kVersion = 1; // We can bump this and let the interface evolve | ||||
|  | ||||
|             PyObject *pyArgs = PyTuple_New(2); | ||||
|             PyTuple_SetItem(pyArgs, 0, PyLong_FromLong(kVersion)); // First argument | ||||
|  | ||||
|             // | ||||
|             // It would be better to create a Python object (a dictionary)  | ||||
|             // from the json msg, but it is simpler to serialize it to a string | ||||
|             // and decode it on the Python side of the fence | ||||
|             // | ||||
|             PyObject* pySerializedJson = PyUnicode_FromString(msg.toStyledString().c_str()); | ||||
|             PyTuple_SetItem(pyArgs, 1, pySerializedJson); // Second argument | ||||
|  | ||||
|             // Invoke the python routine | ||||
|             PyObject* pyList = PyObject_CallObject(pyFunc, pyArgs); | ||||
|  | ||||
|             // Error calling the function | ||||
|             if (pyList == nullptr) | ||||
|             { | ||||
|                 fatalCobraError = true; | ||||
|                 CoreLogger::error("run() function call failed. Input msg: "); | ||||
|                 auto serializedMsg = msg.toStyledString(); | ||||
|                 CoreLogger::error(serializedMsg); | ||||
|                 PyErr_Print(); | ||||
|                 CoreLogger::error("================"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Invalid return type | ||||
|             if (!PyList_Check(pyList)) | ||||
|             { | ||||
|                 fatalCobraError = true; | ||||
|                 CoreLogger::error("run() return type should be a list"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // The result is a list of dict containing sufficient info  | ||||
|             // to send messages to statsd | ||||
|             auto listSize = PyList_Size(pyList); | ||||
|              | ||||
|             for (Py_ssize_t i = 0 ; i < listSize; ++i) | ||||
|             { | ||||
|                 PyObject* dict = PyList_GetItem(pyList, i); | ||||
|  | ||||
|                 // Make sure this is a dict | ||||
|                 if (!PyDict_Check(dict)) | ||||
|                 { | ||||
|                     fatalCobraError = true; | ||||
|                     CoreLogger::error("list element is not a dict"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // | ||||
|                 // Retrieve object kind | ||||
|                 // | ||||
|                 PyObject* pyKind = PyDict_GetItemString(dict, "kind"); | ||||
|                 if (!PyUnicode_Check(pyKind)) | ||||
|                 { | ||||
|                     fatalCobraError = true; | ||||
|                     CoreLogger::error("kind entry is not a string"); | ||||
|                     continue; | ||||
|                 } | ||||
|                 std::string kind(PyUnicode_AsUTF8(pyKind)); | ||||
|  | ||||
|                 bool counter = false; | ||||
|                 bool gauge = false; | ||||
|                 bool timing = false; | ||||
|  | ||||
|                 if (kind == "counter") | ||||
|                 { | ||||
|                     counter = true; | ||||
|                 } | ||||
|                 else if (kind == "gauge") | ||||
|                 { | ||||
|                     gauge = true; | ||||
|                 } | ||||
|                 else if (kind == "timing") | ||||
|                 { | ||||
|                     timing = true; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     fatalCobraError = true; | ||||
|                     CoreLogger::error(std::string("invalid kind entry: ") + kind + | ||||
|                                       ". Supported ones are counter, gauge, timing"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // | ||||
|                 // Retrieve object key | ||||
|                 // | ||||
|                 PyObject* pyKey = PyDict_GetItemString(dict, "key"); | ||||
|                 if (!PyUnicode_Check(pyKey)) | ||||
|                 { | ||||
|                     fatalCobraError = true; | ||||
|                     CoreLogger::error("key entry is not a string"); | ||||
|                     continue; | ||||
|                 } | ||||
|                 std::string key(PyUnicode_AsUTF8(pyKey)); | ||||
|  | ||||
|                 // | ||||
|                 // Retrieve object value and send data to statsd | ||||
|                 // | ||||
|                 PyObject* pyValue = PyDict_GetItemString(dict, "value"); | ||||
|  | ||||
|                 // Send data to statsd | ||||
|                 if (PyFloat_Check(pyValue)) | ||||
|                 { | ||||
|                     double value = PyFloat_AsDouble(pyValue); | ||||
|  | ||||
|                     if (counter) | ||||
|                     { | ||||
|                         statsdClient.count(key, value); | ||||
|                     } | ||||
|                     else if (gauge) | ||||
|                     { | ||||
|                         statsdClient.gauge(key, value); | ||||
|                     } | ||||
|                     else if (timing) | ||||
|                     { | ||||
|                         statsdClient.timing(key, value); | ||||
|                     } | ||||
|                 } | ||||
|                 else if (PyLong_Check(pyValue)) | ||||
|                 { | ||||
|                     long value = PyLong_AsLong(pyValue); | ||||
|  | ||||
|                     if (counter) | ||||
|                     { | ||||
|                         statsdClient.count(key, value); | ||||
|                     } | ||||
|                     else if (gauge) | ||||
|                     { | ||||
|                         statsdClient.gauge(key, value); | ||||
|                     } | ||||
|                     else if (timing) | ||||
|                     { | ||||
|                         statsdClient.timing(key, value); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     fatalCobraError = true; | ||||
|                     CoreLogger::error("value entry is neither an int or a float"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 sentCount++; // should we update this for each statsd object sent ? | ||||
|             } | ||||
|  | ||||
|             Py_DECREF(pyArgs); | ||||
|             Py_DECREF(pyList); | ||||
|         }); | ||||
|  | ||||
|         bool status = bot.run(config); | ||||
|  | ||||
|         // Cleanup - we should do something similar in all exit case ... | ||||
|         Py_DECREF(pyFunc); | ||||
|         Py_DECREF(pyModule); | ||||
|         Py_FinalizeEx(); | ||||
|  | ||||
|         return status; | ||||
|     } | ||||
| #endif | ||||
| } | ||||
							
								
								
									
										19
									
								
								ixbots/ixbots/IXCobraToPythonBot.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								ixbots/ixbots/IXCobraToPythonBot.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| /* | ||||
|  *  IXCobraMetricsToStatsdBot.h | ||||
|  *  Author: Benjamin Sergeant | ||||
|  *  Copyright (c) 2020 Machine Zone, Inc. All rights reserved. | ||||
|  */ | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <ixbots/IXStatsdClient.h> | ||||
| #include "IXCobraBotConfig.h" | ||||
| #include <stddef.h> | ||||
| #include <string> | ||||
|  | ||||
| namespace ix | ||||
| { | ||||
|     int64_t cobra_to_python_bot(const ix::CobraBotConfig& config, | ||||
|                                 StatsdClient& statsdClient, | ||||
|                                 const std::string& scriptPath); | ||||
| } // namespace ix | ||||
		Reference in New Issue
	
	Block a user