diff --git a/Makefile b/Makefile index 3746de2..0d3c2e8 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,10 @@ include $(TOP)/configure/CONFIG DIRS := configure DIRS += caPutLogApp +DIRS += test caPutLogApp_DEPEND_DIRS = configure +test_DEPEND_DIRS = caPutLogApp # Allow 'make docs' but don't otherwise descend into it ifeq ($(MAKECMDGOALS),docs) diff --git a/caPutLogApp/Makefile b/caPutLogApp/Makefile index 17bd8fb..b9272f9 100644 --- a/caPutLogApp/Makefile +++ b/caPutLogApp/Makefile @@ -5,6 +5,8 @@ include $(TOP)/configure/CONFIG LIBRARY_IOC = caPutLog +USR_CPPFLAGS += -DUSE_TYPED_RSET + caPutLog_SRCS += caPutLogTask.c caPutLog_SRCS += caPutLogAs.c caPutLog_SRCS += caPutLogClient.c @@ -21,6 +23,17 @@ INC += caPutLogAs.h DBD += caPutLog.dbd +# Add support for json format and arrays +# This requires EPICS base version 7.0.1 or higher +ifdef BASE_7_0 +USR_CPPFLAGS += -DJSON_AND_ARRAYS_SUPPORTED +caPutLog_SRCS += caPutJsonLogTask.cpp +caPutLog_SRCS += caPutJsonLogShellCommands.cpp +INC += caPutJsonLogTask.h +DBD += caPutJsonLog.dbd +endif + + caPutLog_LIBS += $(EPICS_BASE_IOC_LIBS) caPutLog_SYS_LIBS_WIN32 += ws2_32 diff --git a/caPutLogApp/caPutJsonLog.dbd b/caPutLogApp/caPutJsonLog.dbd new file mode 100644 index 0000000..685335f --- /dev/null +++ b/caPutLogApp/caPutJsonLog.dbd @@ -0,0 +1,2 @@ +variable(caPutLogJsonMsgQueueSize,int) +registrar(caPutJsonLogRegister) diff --git a/caPutLogApp/caPutJsonLogShellCommands.cpp b/caPutLogApp/caPutJsonLogShellCommands.cpp new file mode 100644 index 0000000..1f271e0 --- /dev/null +++ b/caPutLogApp/caPutJsonLogShellCommands.cpp @@ -0,0 +1,94 @@ +/* File: caPutJsonLogShellCommands.cpp + * Author: Matic Pogacnik + * Created: 21.07.2020 + * + * Contains code for a IOC shell configuration commands of the + * JSON CA put logger implementation. + * + * Modification log: + * ---------------- + * v 1.0 + * - Initial version +*/ + +#include +#include +#include + +#include "caPutJsonLogTask.h" + +/* EPICS iocsh shell commands */ +extern "C" +{ + extern int caPutLogJsonMsgQueueSize; + + /* Initalisation */ + int caPutJsonLogInit(const char * address, caPutJsonLogConfig config){ + CaPutJsonLogTask *logger = CaPutJsonLogTask::getInstance(); + if (logger != NULL) return logger->initialize(address, config); + else return -1; + } + + static const iocshArg caPutJsonLogInitArg0 = {"address", iocshArgString}; + static const iocshArg caPutJsonLogInitArg1 = {"config", iocshArgInt}; + static const iocshArg *const caPutJsonLogInitArgs[] = { + &caPutJsonLogInitArg0, + &caPutJsonLogInitArg1 + }; + static const iocshFuncDef caPutjsonLogInitDef = {"caPutJsonLogInit", 2, caPutJsonLogInitArgs}; + static void caPutJsonLogInitCall(const iocshArgBuf *args) + { + caPutJsonLogInit(args[0].sval, static_cast(args[1].ival)); + } + + + /* Reconfigure */ + int caPutJsonLogReconf(caPutJsonLogConfig config){ + CaPutJsonLogTask *logger = CaPutJsonLogTask::getInstance(); + if (logger != NULL) return logger->reconfigure(config); + else return -1; + } + + static const iocshArg caPutJsonLogReconfArg0 = {"config", iocshArgInt}; + static const iocshArg *const caPutJsonLogReconfArgs[] = { + &caPutJsonLogReconfArg0 + }; + static const iocshFuncDef caPutJsonLogReconfDef = {"caPutJsonLogReconf", 1, caPutJsonLogReconfArgs}; + static void caPutJsonLogReconfCall(const iocshArgBuf *args) + { + caPutJsonLogReconf(static_cast(args[0].ival)); + } + + /* Report */ + int caPutJsonLogShow(int level){ + CaPutJsonLogTask *logger = CaPutJsonLogTask::getInstance(); + if (logger != NULL) return logger->report(level); + else return -1; + } + + static const iocshArg caPutJsonLogShowArg0 = {"level", iocshArgInt}; + static const iocshArg *const caPutJsonLogShowArgs[] = { + &caPutJsonLogShowArg0 + }; + static const iocshFuncDef caPutJsonLogShowDef = {"caPutJsonLogShow", 1, caPutJsonLogShowArgs}; + static void caPutJsonLogShowCall(const iocshArgBuf *args) + { + caPutJsonLogShow(args[0].ival); + } + + + /* Register IOCsh commands */ + static void caPutJsonLogRegister(void) + { + static int done = FALSE; + if(done) return; + done = TRUE; + + iocshRegister(&caPutjsonLogInitDef,caPutJsonLogInitCall); + iocshRegister(&caPutJsonLogReconfDef,caPutJsonLogReconfCall); + iocshRegister(&caPutJsonLogShowDef,caPutJsonLogShowCall); + } + epicsExportRegistrar(caPutJsonLogRegister); + + epicsExportAddress(int,caPutLogJsonMsgQueueSize); +} diff --git a/caPutLogApp/caPutJsonLogTask.cpp b/caPutLogApp/caPutJsonLogTask.cpp new file mode 100644 index 0000000..2073f69 --- /dev/null +++ b/caPutLogApp/caPutJsonLogTask.cpp @@ -0,0 +1,764 @@ +/* File: caPutJsonLogTask.cpp + * Author: Matic Pogacnik + * Created: 21.07.2020 + * + * Implementation of a CaPutJsonLogTask class. + * For more information refer to the header file. + * + * Modification log: + * ---------------- + * v 1.0 + * - Initial version. +*/ + +// Standard library imports +#include +#include +#include + +// Epics Base imports +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// This module imports +#include "caPutLogAs.h" +#include "caPutLogTask.h" +#include "caPutJsonLogTask.h" + + +#define isDbrNumeric(type) ((type) > DBR_STRING && (type) <= DBR_ENUM) + +int caPutLogJsonMsgQueueSize = 1000; +static const ENV_PARAM EPICS_CA_JSON_PUT_LOG_ADDR = {epicsStrDup("EPICS_CA_JSON_PUT_LOG_ADDR"), epicsStrDup("")}; + +CaPutJsonLogTask * CaPutJsonLogTask::instance = NULL; + + +CaPutJsonLogTask *CaPutJsonLogTask::getInstance() +{ + if (instance == NULL) { + try{ + instance = new CaPutJsonLogTask(); + } + catch (...) { + errlogSevPrintf(errlogMajor, "caPutJsonLog: Failed to construct CA put JSON logger\n"); + return NULL; + } + } + return instance; +} + +CaPutJsonLogTask::CaPutJsonLogTask() + : caPutJsonLogQ(caPutLogJsonMsgQueueSize, sizeof(LOGDATA *)), + threadId(NULL), + taskStopper(false), + caPutJsonLogClient(NULL), + pCaPutJsonLogPV(NULL) +{ } + +CaPutJsonLogTask::~CaPutJsonLogTask() +{ } + +caPutJsonLogStatus CaPutJsonLogTask::reconfigure(caPutJsonLogConfig config) +{ + if ((config < caPutJsonLogNone) + || (config > caPutJsonLogAllNoFilter)) { + errlogSevPrintf(errlogMinor, "caPutJsonLog: invalid config request, setting to default 'caPutJsonLogAll'\n"); + epics::atomic::set(this->config, caPutJsonLogAll); + } else { + epics::atomic::set(this->config, config); + } + return caPutJsonLogSuccess; +} + +caPutJsonLogStatus CaPutJsonLogTask::report(int level) +{ + if (this->caPutJsonLogClient != NULL) { + logClientShow(this->caPutJsonLogClient, level); + return caPutJsonLogSuccess; + } + else { + errlogSevPrintf(errlogMinor, "caPutJsonLog: log client not initialised\n"); + return caPutJsonLogError; + } +} + +caPutJsonLogStatus CaPutJsonLogTask::initialize(const char* address, caPutJsonLogConfig config) +{ + caPutJsonLogStatus status; + + // Store passed configuration parameters + this->address = epicsStrDup(address); + this->reconfigure(config); + + // Check if user enabled the logger + if (config == caPutJsonLogNone) { + errlogSevPrintf(errlogInfo, "caPutJsonLog: disabled\n"); + return caPutJsonLogSuccess; + } + + //Configure PV logging + this->configurePvLogging(); + + // Initialize server logging + if (this->caPutJsonLogClient == NULL) { + status = configureServerLogging(address); + if (status != caPutJsonLogSuccess) return status; + } + + // Start logger + status = this->start(); + if (status != caPutJsonLogSuccess) { + return status; + } + + // Initialize caPutLogAs + status = static_cast(caPutLogAsInit(caddPutToQueue, NULL)); + if (status != caPutJsonLogSuccess) { + errlogSevPrintf(errlogMinor, "caPutJsonLog: failed to configure Access security\n"); + return caPutJsonLogError; + } + + // Register exit handler + epicsAtExit(caPutJsonLogExit, NULL); + + return caPutJsonLogSuccess; +} + +caPutJsonLogStatus CaPutJsonLogTask::start() +{ + // Check if Access security is enabled + if (!asActive) { + errlogSevPrintf(errlogMajor, "caPutJsonLog: access security disabled, exiting now\n"); + return caPutJsonLogError; + } + + // Create logging thread + epics::atomic::set(this->taskStopper, false); + const char * threadName = "caPutJsonLog"; + threadId = epicsThreadCreate(threadName, + epicsThreadPriorityLow, + epicsThreadGetStackSize(epicsThreadStackSmall), + (EPICSTHREADFUNC) caPutJsonLogWorker, + NULL); + if (!threadId) { + errlogSevPrintf(errlogFatal,"caPutJsonLog: thread creation failed\n"); + return caPutJsonLogError; + } + return caPutJsonLogSuccess; +} + +caPutJsonLogStatus CaPutJsonLogTask::stop() +{ + // Send signal to stop the logger worker thread + epics::atomic::set(this->taskStopper, true); + + // Deregister Access Security trap + caPutLogAsStop(); + + return caPutJsonLogSuccess; +} + +caPutJsonLogStatus CaPutJsonLogTask::configureServerLogging(const char* address) +{ + int status; + struct sockaddr_in saddr; + + // Parse the address + if (!address || !address[0]) { + address = envGetConfigParamPtr(&EPICS_CA_JSON_PUT_LOG_ADDR); + } + if (address == NULL) { + errlogSevPrintf(errlogMajor, "caPutJsonLog: server address not specified\n"); + return caPutJsonLogError; + } + + status = aToIPAddr(address, this->default_port, &saddr); + if (status < 0) { + errlogSevPrintf(errlogMajor, "caPutJsonLog: bad address or host name\n"); + return caPutJsonLogError; + } + + // Create log client + this->caPutJsonLogClient = logClientCreate(saddr.sin_addr, ntohs(saddr.sin_port)); + if (this->caPutJsonLogClient == NULL) return caPutJsonLogError; + + return caPutJsonLogSuccess; +} + +caPutJsonLogStatus CaPutJsonLogTask::configurePvLogging() +{ + char *caPutJsonLogPVEnv; + + // Get name of a PV to which to log + caPutJsonLogPVEnv = std::getenv("EPICS_AS_PUT_JSON_LOG_PV"); + + // Variable is not set, return success (Logging to a PV is disabled) + if (!caPutJsonLogPVEnv){ + this->pCaPutJsonLogPV = NULL; + return caPutJsonLogSuccess; + } + + // Get pointer to a PV structure + long getpv_st; + this->pCaPutJsonLogPV = &this->caPutJsonLogPV; + getpv_st = dbNameToAddr(caPutJsonLogPVEnv, this->pCaPutJsonLogPV); + + if (getpv_st) { + this->pCaPutJsonLogPV = NULL; + errlogSevPrintf(errlogMajor, + "caPutJsonLog: PV for CA Put Logging not found, logging to PV disabled\n"); + return caPutJsonLogError; + } + return caPutJsonLogSuccess; +} + +void CaPutJsonLogTask::caPutJsonLogTask(void *arg) +{ + + bool sent = false, burst = false; + LOGDATA *pcurrent, *pnext; + VALUE old_value, max_value, min_value; + VALUE *pold=&old_value, *pmax=&max_value, *pmin=&min_value; + + // Receive 1st message + this->caPutJsonLogQ.receive(&pcurrent, sizeof(LOGDATA *)); + std::memcpy(pold, &pcurrent->old_value, sizeof(VALUE)); + + if (pcurrent->new_size > 0 && pcurrent->type != DBR_CHAR) { + std::memcpy(pmax, &pcurrent->new_value.value, sizeof(VALUE)); + std::memcpy(pmin, &pcurrent->new_value.value, sizeof(VALUE)); + } + + // Main loop of the logger, which accepts the caput changes and process them + while (!(bool)epics::atomic::get(this->taskStopper)) + { + int msgSize; + + // Receive new put with timeout + msgSize = this->caPutJsonLogQ.receive(&pnext, sizeof(LOGDATA *), 5.0); + + /* Timeout */ + if (msgSize == -1) { + // If we have have unsent message and timeout occurred, send the cached change + if (!sent) { + buildJsonMsg(pold, pcurrent, burst, pmin, pmax); + std::memcpy(pold, &pcurrent->new_value.value, sizeof(VALUE)); + sent = true; + burst = false; + } + } + + // Message is not the correct size + else if (msgSize != sizeof(LOGDATA *)) { + errlogSevPrintf(errlogMinor, "caPutJsonLog: discarding incomplete log data message\n"); + } + + // Previous and new PV are the same and we are applying the burst filter + else if ((pnext->pfield == pcurrent->pfield) + && (epics::atomic::get(this->config) != caPutJsonLogAllNoFilter) + && (pcurrent->type != DBR_CHAR)) { + + // Free "old" value + caPutLogDataFree(pcurrent); + pcurrent = pnext; + + // First message after logging + if (sent) { + // Set new initial max & min values + std::memcpy(pmax, &pcurrent->new_value.value, sizeof(VALUE)); + std::memcpy(pmin, &pcurrent->new_value.value, sizeof(VALUE)); + + sent = false; + burst = false; + } + // Multiple puts within timeout + else { + if (isDbrNumeric(pcurrent->type) && pcurrent->new_size == 1) { + burst = true; + calculateMax(pmax, &pcurrent->new_value.value, pmax, pcurrent->type); + calculateMin(pmin, &pcurrent->new_value.value, pmin, pcurrent->type); + } + } + } + + // We log every change + else { + if (!sent) { + buildJsonMsg(pold, pcurrent, burst, pmin, pmax); + sent = true; + } + + caPutLogDataFree(pcurrent); + pcurrent = pnext; + + /* Set new old_value */ + std::memcpy(pold, &pcurrent->old_value, sizeof(VALUE)); + + /* Set new max & min values */ + std::memcpy(pmax, &pcurrent->new_value.value, sizeof(VALUE)); + std::memcpy(pmin, &pcurrent->new_value.value, sizeof(VALUE)); + + sent = false; + burst = false; + } + } + epics::atomic::set(this->taskStopper, false); + errlogSevPrintf(errlogInfo, "caPutJsonLog: log task exiting\n"); +} + +void CaPutJsonLogTask::addPutToQueue(LOGDATA * plogData) +{ + if (this->caPutJsonLogQ.trySend(&plogData, sizeof(LOGDATA *))) { + errlogSevPrintf(errlogMinor, "caPutJsonLog: message queue overflow\n"); + caPutLogDataFree(plogData); + } +} + + +#define CALL_YAJL_FUNCTION_AND_CHECK_STATUS(flag, call) \ + { \ + flag = call; \ + if (flag != yajl_gen_status_ok) { \ + errlogSevPrintf(errlogMinor, "caPutJsonLog: JSON generation error\n"); \ + return caPutJsonLogError; \ + } \ + } + +caPutJsonLogStatus CaPutJsonLogTask::buildJsonMsg(const VALUE *pold_value, const LOGDATA *pLogData, + bool burst, const VALUE *pmin, const VALUE *pmax) +{ + // Intermediate message build buffer + // The longest message for the buffer can occur in the lso/lsi records which + // is defined with MAX_ARRAY_SIZE_BYTES, if this is less then 40 then + // stringin / stringout are the limits + size_t interBufferSize = MAX_STRING_SIZE + 1 > MAX_ARRAY_SIZE_BYTES + 1 + ? MAX_STRING_SIZE + 1 + : MAX_ARRAY_SIZE_BYTES + 1; + unsigned char interBuffer[interBufferSize]; + yajl_gen_status status; + + // Configure yajl generator + yajl_gen handle = yajl_gen_alloc(NULL); + if (handle == NULL) { + errlogSevPrintf(errlogMinor, "caPutJsonLog: failed to allocate yajl handler\n"); + return caPutJsonLogError; + } + + // Open json root map + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_map_open(handle)); + + // Add date parameter + const unsigned char str_date[] = "date"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_date, + strlen(reinterpret_cast(str_date)))); + epicsTimeToStrftime(reinterpret_cast(interBuffer), interBufferSize, "%Y-%m-%d", + &pLogData->new_value.time); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, interBuffer, + strlen(reinterpret_cast(interBuffer)))); + + // Add time of the day parameter + const unsigned char str_time[] = "time"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_time, + strlen(reinterpret_cast(str_time)))); + epicsTimeToStrftime(reinterpret_cast(interBuffer), interBufferSize, "%H:%M:%S.%03f", + &pLogData->new_value.time); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, interBuffer, + strlen(reinterpret_cast(interBuffer)))); + + // Add hostname + const unsigned char str_host[] = "host"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_host, + strlen(reinterpret_cast(str_host)))); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, + reinterpret_cast(pLogData->hostid), + strlen(pLogData->hostid))); + + // Add system user + const unsigned char str_user[] = "user"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_user, + strlen(reinterpret_cast(str_user)))); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, + reinterpret_cast(pLogData->userid), + strlen(pLogData->userid))); + + // Add PV name + const unsigned char str_pvName[] = "pv"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_pvName, + strlen(reinterpret_cast(str_pvName)))); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, + reinterpret_cast(pLogData->pv_name), + strlen(pLogData->pv_name))); + + // Add new PV value */ + const unsigned char str_newVal[] = "new"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_newVal, + strlen(reinterpret_cast(str_newVal)))); + + // Open Json array if we have array value + if (pLogData->is_array) { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_array_open(handle)); + } + + // We have string + if (pLogData->type == DBR_CHAR){ + fieldVal2Str(reinterpret_cast(interBuffer), interBufferSize, + &pLogData->new_value.value, DBR_CHAR, 0); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, interBuffer, + strlen(reinterpret_cast(interBuffer)))); + } + // Arrays and scalars (all except DBR_CHAR) + else { + for (int i = 0; i < pLogData->new_log_size; i++) { + if (this->testForSpecialValues(&pLogData->new_value.value, pLogData->type, i) == svNan){ + const unsigned char str_Nan[] = "Nan"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_Nan, + strlen(reinterpret_cast(str_Nan)))); + } + else if (this->testForSpecialValues(&pLogData->new_value.value, pLogData->type, i) == svPinf){ + const unsigned char str_pinf[] = "Infinity"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_pinf, + strlen(reinterpret_cast(str_pinf)))); + } + else if (this->testForSpecialValues(&pLogData->new_value.value, pLogData->type, i)== svNinf){ + const unsigned char str_ninf[] = "-Infinity"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_ninf, + strlen(reinterpret_cast(str_ninf)))); + } + else { + fieldVal2Str(reinterpret_cast(interBuffer), interBufferSize, + &pLogData->new_value.value, pLogData->type, i); + if (pLogData->type == DBR_STRING) { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, interBuffer, + strlen(reinterpret_cast(interBuffer)))); + } else { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_number(handle, + reinterpret_cast(interBuffer), + strlen(reinterpret_cast(interBuffer)))); + } + } + } + } + // Close Json array and add new size, but only if we have array + if (pLogData->is_array) { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_array_close(handle)); + const unsigned char str_newSize[] = "new-size"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_newSize, + strlen(reinterpret_cast(str_newSize)))); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_integer(handle, pLogData->new_size)); + } + + // Add old PV value */ + const unsigned char str_oldVal[] = "old"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_oldVal, + strlen(reinterpret_cast(str_oldVal)))); + // Open Json array if we have array value + if (pLogData->is_array) { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_array_open(handle)); + } + // We have string + if (pLogData->type == DBR_CHAR){ + fieldVal2Str(reinterpret_cast(interBuffer), interBufferSize, + pold_value, DBR_CHAR, 0); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, interBuffer, + strlen(reinterpret_cast(interBuffer)))); + } + // Arrays and scalars (all except DBR_CHAR) + else { + for (int i = 0; i < pLogData->old_log_size; i++) { + if (this->testForSpecialValues(pold_value, pLogData->type, i) == svNan){ + const unsigned char str_Nan[] = "Nan"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_Nan, + strlen(reinterpret_cast(str_Nan)))); + } + else if (this->testForSpecialValues(pold_value, pLogData->type, i) == svPinf){ + const unsigned char str_pinf[] = "Infinity"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_pinf, + strlen(reinterpret_cast(str_pinf)))); + } + else if (this->testForSpecialValues(pold_value, pLogData->type, i) == svNinf){ + const unsigned char str_ninf[] = "-Infinity"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_ninf, + strlen(reinterpret_cast(str_ninf)))); + } + else { + fieldVal2Str(reinterpret_cast(interBuffer), interBufferSize, + pold_value, pLogData->type, i); + if (pLogData->type == DBR_STRING) { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, interBuffer, + strlen(reinterpret_cast(interBuffer)));); + } else { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_number(handle, + reinterpret_cast(interBuffer), + strlen(reinterpret_cast(interBuffer)))); + } + } + } + } + // Close Json array and add new size, but only if we have array + if (pLogData->is_array) { + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_array_close(handle)); + const unsigned char str_oldSize[] = "old-size"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_oldSize, + strlen(reinterpret_cast(str_oldSize)))); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_integer(handle, pLogData->old_size)); + } + + // Add minium and maximum values in case of a burst + if (burst && isDbrNumeric(pLogData->type) + && !pLogData->is_array) { + // Add min value + const unsigned char str_minVal[] = "min"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_minVal, + strlen(reinterpret_cast(str_minVal)))); + fieldVal2Str(reinterpret_cast(interBuffer), interBufferSize, + pmin, pLogData->type, 0); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_number(handle, + reinterpret_cast(interBuffer), + strlen(reinterpret_cast(interBuffer)))); + + // Add max value + const unsigned char str_maxVal[] = "max"; + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_string(handle, str_maxVal, + strlen(reinterpret_cast(str_maxVal)));); + fieldVal2Str(reinterpret_cast(interBuffer), interBufferSize, + pmax, pLogData->type, 0); + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_number(handle, + reinterpret_cast(interBuffer), + strlen(reinterpret_cast(interBuffer)))); + } + + /* Close root map */ + CALL_YAJL_FUNCTION_AND_CHECK_STATUS(status, yajl_gen_map_close(handle)); + + /* Get JSON as NULL terminated cstring */ + const unsigned char * buf; + size_t len = 0; + yajl_gen_get_buf(handle, &buf, &len); + + /* Get a JSON as a string */ + std::string json (reinterpret_cast(buf)); + + /* First log to a PV so we can append new line later for the logging to a server */ + this->logToPV(json); + this->logToServer(json.append("\n")); + return caPutJsonLogSuccess; +} + +void CaPutJsonLogTask::logToServer(std::string &msg) +{ + if (caPutJsonLogClient) { + logClientSend(caPutJsonLogClient, msg.c_str()); + } +} + +void CaPutJsonLogTask::logToPV(std::string &msg) +{ + if (this->pCaPutJsonLogPV != NULL) { + long status; + + // Waveform record + if (this->pCaPutJsonLogPV->field_type == DBR_CHAR) { + status = dbPutField(this->pCaPutJsonLogPV, DBR_CHAR, msg.c_str(), msg.length()); + } + // Lso/lsi records + else { + // As of EPICS base 7.0.4 this still clips at 40 characters + // Addint .$ or .VAL$ to the PV name, will use the waveform case and + // write the whole string correctly + status = dbPutField(this->pCaPutJsonLogPV, DBR_STRING, msg.c_str(), 1); + } + + if (status) { + errlogSevPrintf(errlogMajor, + "caPutJsonLog: dbPutField to Log PV failed, status = %ld\n", status); + } + } +} + +void CaPutJsonLogTask::calculateMin(VALUE *pres, const VALUE *pa, const VALUE *pb, short type) +{ + switch (type) { + case DBR_CHAR: + pres->v_int8 = std::min(pa->v_int8, pb->v_int8); + return; + case DBR_UCHAR: + pres->v_uint8 = std::min(pa->v_uint8, pb->v_uint8); + return; + case DBR_SHORT: + pres->v_int16 = std::min(pa->v_int16, pb->v_int16); + return; + case DBR_USHORT: + case DBR_ENUM: + pres->v_uint16 = std::min(pa->v_uint16, pb->v_uint16); + return; + case DBR_LONG: + pres->v_int32 = std::min(pa->v_int32, pb->v_int32); + return; + case DBR_ULONG: + pres->v_uint32 = std::min(pa->v_uint32, pb->v_uint32); + return; + case DBR_INT64: + pres->v_int64 = std::min(pa->v_int64, pb->v_int64); + return; + case DBR_UINT64: + pres->v_uint64 = std::min(pa->v_uint64, pb->v_uint64); + return; + case DBR_FLOAT: + pres->v_float = std::min(pa->v_float, pb->v_float); + return; + case DBR_DOUBLE: + pres->v_double = std::min(pa->v_double, pb->v_double); + return; + } +} + +void CaPutJsonLogTask::calculateMax(VALUE *pres, const VALUE *pa, const VALUE *pb, short type) +{ + switch (type) { + case DBR_CHAR: + pres->v_int8 = std::max(pa->v_int8, pb->v_int8); + return; + case DBR_UCHAR: + pres->v_uint8 = std::max(pa->v_uint8, pb->v_uint8); + return; + case DBR_SHORT: + pres->v_int16 = std::max(pa->v_int16, pb->v_int16); + return; + case DBR_USHORT: + case DBR_ENUM: + pres->v_uint16 = std::max(pa->v_uint16, pb->v_uint16); + return; + case DBR_LONG: + pres->v_int32 = std::max(pa->v_int32, pb->v_int32); + return; + case DBR_ULONG: + pres->v_uint32 = std::max(pa->v_uint32, pb->v_uint32); + return; + case DBR_INT64: + pres->v_int64 = std::max(pa->v_int64, pb->v_int64); + return; + case DBR_UINT64: + pres->v_uint64 = std::max(pa->v_uint64, pb->v_uint64); + return; + case DBR_FLOAT: + pres->v_float = std::max(pa->v_float, pb->v_float); + return; + case DBR_DOUBLE: + pres->v_double = std::max(pa->v_double, pb->v_double); + return; + } +} + +bool CaPutJsonLogTask::compareValue(const VALUE *pa, const VALUE *pb, short type) +{ + switch (type) { + case DBR_CHAR: + return (pa->v_int8 == pb->v_int8); + case DBR_UCHAR: + return (pa->v_uint8 == pb->v_uint8); + case DBR_SHORT: + return (pa->v_int16 == pb->v_int16); + case DBR_USHORT: + case DBR_ENUM: + return (pa->v_uint16 == pb->v_uint16); + case DBR_LONG: + return (pa->v_int32 == pb->v_int32); + case DBR_ULONG: + return (pa->v_uint32 == pb->v_uint32); + case DBR_INT64: + return (pa->v_int64 == pb->v_int64); + case DBR_UINT64: + return (pa->v_uint64 == pb->v_uint64); + case DBR_FLOAT: + return (pa->v_float == pb->v_float); + case DBR_DOUBLE: + return (pa->v_double == pb->v_double); + case DBR_STRING: + return (0 == strcmp(pa->v_string, pb->v_string)); + default: + return 0; + } +} + +int CaPutJsonLogTask::fieldVal2Str(char *pbuf, size_t buflen, const VALUE *pval, short type, int index) +{ + switch (type) { + case DBR_CHAR: + return epicsSnprintf(pbuf, buflen, "%s", ((char *)pval)); + case DBR_UCHAR: + return epicsSnprintf(pbuf, buflen, "%d", ((epicsUInt8 *)pval)[index]); + case DBR_SHORT: + return epicsSnprintf(pbuf, buflen, "%hd", ((epicsInt16 *)pval)[index]); + case DBR_USHORT: + case DBR_ENUM: + return epicsSnprintf(pbuf, buflen, "%hu", ((epicsUInt16 *)pval)[index]); + case DBR_LONG: + return epicsSnprintf(pbuf, buflen, "%d", ((epicsInt32 *)pval)[index]); + case DBR_ULONG: + return epicsSnprintf(pbuf, buflen, "%u", ((epicsUInt32 *)pval)[index]); + case DBR_FLOAT: + return epicsSnprintf(pbuf, buflen, "%g", ((epicsFloat32 *)pval)[index]); + case DBR_DOUBLE: + return epicsSnprintf(pbuf, buflen, "%g", ((epicsFloat64 *)pval)[index]); + case DBR_INT64: + return epicsSnprintf(pbuf, buflen, "%lld", ((epicsInt64 *)pval)[index]); + case DBR_UINT64: + return epicsSnprintf(pbuf, buflen, "%llu", ((epicsUInt64 *)pval)[index]); + case DBR_STRING: + return epicsSnprintf(pbuf, buflen, "%s", ((char *)pval) + index * MAX_STRING_SIZE); + default: + errlogSevPrintf(errlogMajor, + "caPutJsonLog: failed to convert PV value to a string representation\n"); + return caPutJsonLogError; + } +} + +specialValues CaPutJsonLogTask::testForSpecialValues(const VALUE *pval, short type, int index) { + + epicsFloat64 d; + + switch (type) { + case DBR_FLOAT: + d = ((epicsFloat32 *)pval)[index]; + break; + case DBR_DOUBLE: + d = ((epicsFloat64 *)pval)[index]; + break; + default: + return svNormal; + } + + if (isnan(d)) return svNan; + else if (isinf(d) && d > 0) return svPinf; + else if (isinf(d) && d < 0) return svNinf; + else return svNormal; +} + + +/* Called from C */ + +void caddPutToQueue(LOGDATA * plogData) +{ + CaPutJsonLogTask *instance = CaPutJsonLogTask::getInstance(); + return instance->addPutToQueue(plogData); +} +void caPutJsonLogWorker(void *arg) +{ + CaPutJsonLogTask *instance = CaPutJsonLogTask::getInstance(); + instance->caPutJsonLogTask(arg); +} +void caPutJsonLogExit(void *arg) +{ + CaPutJsonLogTask *instance = CaPutJsonLogTask::getInstance(); + instance->stop(); +} diff --git a/caPutLogApp/caPutJsonLogTask.h b/caPutLogApp/caPutJsonLogTask.h new file mode 100644 index 0000000..4da0adf --- /dev/null +++ b/caPutLogApp/caPutJsonLogTask.h @@ -0,0 +1,289 @@ +/* File: caPutJsonLogTask.h + * Author: Matic Pogacnik + * Created: 21.07.2020 + * + * Contains definitions of a CaPutJsonLogTask which is capable of logging CA put + * changes to a remote server and a PV in a JSON format. + * + * Modification log: + * ---------------- + * v 1.0 + * - Initial version +*/ + +#ifndef INCcaPutJsonLogh +#define INCcaPutJsonLogh 1 + +// Standard library imports +#include + +// Epics base imports +#include +#include +#include +#include + +// Includes from this module +#include "caPutLogTask.h" + +// Status return values +enum caPutJsonLogStatus { + caPutJsonLogSuccess = 0, + caPutJsonLogError = -1 +}; + +// Configuration options +enum caPutJsonLogConfig { + caPutJsonLogNone = -1, /* no logging (disable) */ + caPutJsonLogOnChange = 0, /* log only on value change */ + caPutJsonLogAll = 1, /* log all puts */ + caPutJsonLogAllNoFilter = 2 /* log all puts no filtering on same PV*/ +}; + +enum specialValues { + svNormal, + svNan, + svNinf, + svPinf +}; + +#ifdef __cplusplus + +/** + * @brief Implementation of EPICS Channel Access put logger. Log messages are produced + * in JSON format: + * \code{.txt} + * {"date": "
--"","time":"::","host":"","user":"","pv":"","new":,"old":""}\n + * \endcode + * Burst puts add extra JSON properties (supported only for numberic scalar puts): + * - "min": Which is a minimum value inside the burst period + * - "max": Represents maximum value inside the burst of puts + * Array puts add following JSON properties: + * - "new-size": new array length of array (in case of a lso/lsi record this is string length) + * - "old-size": old array length of array (in case of a lso/lsi record this is string length) + * + * Implementation registers trap inside the Access security trap via "caPutLogAs.h" interface. After each caput our + * callback method is called which add a `LOGDATA` structure to the queue. On the logger thread we take the messages and + * check fo the bursts (with timeout). When all required information is known we generate JSON messsage, which is then + * send to the logging server and PV if configured. + * + * This class is designed as a singleton object. + * + * For the instructions how to use the utility please refer to the user manual. + * + */ +class epicsShareClass CaPutJsonLogTask { +public: + + // Default port to be used if not specified by the user + static const int default_port = 7011; + + /** + * @brief Get the singleton Instance object. + * + * @return CaPutJsonLogTask* Pointer to the only instance of the CaPutJsonLogTask object. + */ + static CaPutJsonLogTask* getInstance(); + + /** + * @brief Initialize the object. + * + * @param address IP address or hostname of the log server. Can include a port number after a colon, + * if port number is not specified, default value will be used. + * @param config Configuration paramteter. Valid value are -1 <= config <= 2. + * @return caPutJsonLogStatus Status code. + */ + caPutJsonLogStatus initialize(const char* address, caPutJsonLogConfig config); + + /** + * @brief Add a put details packed as a ::LOGDATA structure to a queue for processing. + * This method registered as a callback inside the caPutLog.h. + * + * @param plogData ::LOGDATA structure holding details about caput. + */ + void addPutToQueue(LOGDATA * plogData); + + /** + * @brief Main loop of the logger. This method is started in a separate thread. + * It takes a caput messages from a queue process them and calls buildJsonMsg(). + * + * @param arg Parameter is not used. Could be in the feature. + */ + void caPutJsonLogTask(void *arg); //Must be public, called from C + + /** + * @brief Start the logging. + * + * @return int Status code. + */ + caPutJsonLogStatus start(); + + /** + * @brief Stop the logging. + * + * @return int Status code. + */ + caPutJsonLogStatus stop(); + + /** + * @brief Reconfigure the logging. + * + * @param config New configuration. Valid value are -1 <= config <= 2. Invalid + * value will default to 1 "caPutJsonLogAll". + * @return int Status code. + */ + caPutJsonLogStatus reconfigure(caPutJsonLogConfig config); + + /** + * @brief Print report client logger information to the console. + * + * @param level Level of details to be printed. + * @return int Status code. + */ + caPutJsonLogStatus report(int level); + +private: + + // Singelton instance of this class. + static CaPutJsonLogTask *instance; + + // Logger configuration + const char * address; + int config; // To modify or read this value only epicsAtomic methods should be used + + // Interthread communication + epicsMessageQueue caPutJsonLogQ; + + // Working thread + epicsThreadId threadId; + int taskStopper; // To modify or read this value only epicsAtomic methods should be used + + //Logging to a server + logClientId caPutJsonLogClient; + + // Logging to a PV + DBADDR caPutJsonLogPV; + DBADDR *pCaPutJsonLogPV; + + + // Class methods (Do not allow public constructors - class is designed as singleton) + CaPutJsonLogTask(); + virtual ~CaPutJsonLogTask(); + CaPutJsonLogTask(const CaPutJsonLogTask&); + + // Commeted as move constructor is c++11 feature, but we want compile on older versions as well + // CaPutJsonLogTask(const CaPutJsonLogTask&&); + + /** + * @brief Build a JSON string from and call logToServer() and logToPV() methods to log a message. + * + * @param pold_value Pointer to a ::VALUE structure holding an old PV value. + * @param pLogData Pointer to a ::LOGDATA structure holding new value and other meta databa about the put. + * @param burst Boolean value. It determinate if put was a burst of values. + * @param pmin Pointer to a ::VALUE structure holding an min value if burst is true. + * @param pmax Pointer to a ::VALUE structure holding an max value if burst is true. + * @return int Status code. + */ + caPutJsonLogStatus buildJsonMsg(const VALUE *pold_value, const LOGDATA *pLogData, + bool burst, const VALUE *pmin, const VALUE *pmax); + + /** + * @brief Configure logging to a server. + * + * @param address IP address or hostname of the logging server. + * @return int Status code. + */ + caPutJsonLogStatus configureServerLogging(const char* address); + + /** + * @brief This method will send a message to the configured log server. + * + * @param msg Message to be send. + */ + void logToServer(std::string &msg); + + /** + * @brief Configure logging to a PV. + * + * @return int Status code. + */ + caPutJsonLogStatus configurePvLogging(); + + /** + * @brief Method will write a message to the configured PV. + * + * @param msg Message to be written. + */ + void logToPV(std::string &msg); + + /** + * @brief Calculate minimum value of two ::VALUE unions. + * + * @param pres ::VALUE structure where result should be written. + * @param pa ::VALUE structure of the first value to be compared. + * @param pb ::VALUE structure of the second value to be compared. + * @param type EPICS DRB_* type stored in the input structures. + */ + void calculateMin(VALUE *pres, const VALUE *pa, const VALUE *pb, short type); + + /** + * @brief Calculate maximum value of two ::VALUE unions. + * + * @param pres ::VALUE structure where result should be written. + * @param pa ::VALUE structure of the first value to be compared. + * @param pb ::VALUE structure of the second value to be compared. + * @param type EPICS DRB_* type stored in the input structures. + */ + void calculateMax(VALUE *pres, const VALUE *pa, const VALUE *pb, short type); + + /** + * @brief Compare values in two ::VALUE structures if they are the same. + * + * @param pa First ::VALUE structure to be compared. + * @param pb Second ::VALUE structure to be compared. + * @param type EPICS DRB_* type stored in the input structures. + * @return true If values are the same. + * @return false If values are not the same. + */ + bool compareValue(const VALUE *pa, const VALUE *pb, short type); + + /** + * @brief Get a string representation of the value stored in the ::VALUE. + * + * @param pbuf Pointer to a character buffer where the output should be written. + * @param buflen Length of the `pbuf`. + * @param pval Pointer to the ::VALUE structure to be transformed to the string representation. + * @param type EPICS DRB_* type stored in the input structure. + * @param index Index of the element in case of an array. For scalar values this must be 0. + * @return int Status return code. + */ + int fieldVal2Str(char *pbuf, size_t buflen, const VALUE *pval, short type, int index); + + /** + * @brief Check for special value (nan, positive/negative infinity) in ::VALUE structure. + * + * @param pval ::VALUE to be checked. + * @param type EPICS DRB_* type stored in the input ::VALUE. + * @param index Index of the element in case of an array. For scalar values this must be 0. + * @return specialValues + */ + specialValues testForSpecialValues(const VALUE *pval, short type, int index); + +}; + + +extern "C" { +#endif /*__cplusplus */ + +/* + * C interface functions (Called from base) + */ +epicsShareFunc void caddPutToQueue(LOGDATA * plogData); +epicsShareFunc void caPutJsonLogWorker(void *arg); +epicsShareFunc void caPutJsonLogExit(void *arg); + +#ifdef __cplusplus +} +#endif /*__cplusplus */ + +#endif /* INCcaPutJsonLogh */ diff --git a/caPutLogApp/caPutLog.c b/caPutLogApp/caPutLog.c index eb7b3ed..abd00fc 100644 --- a/caPutLogApp/caPutLog.c +++ b/caPutLogApp/caPutLog.c @@ -89,7 +89,7 @@ int caPutLogInit (const char *addr_str, int config) return caPutLogError; } - status = caPutLogAsInit(); + status = caPutLogAsInit(caPutLogTaskSend, caPutLogTaskStop); if (status) { return caPutLogError; } diff --git a/caPutLogApp/caPutLogAs.c b/caPutLogApp/caPutLogAs.c index 4a6b966..efdae46 100644 --- a/caPutLogApp/caPutLogAs.c +++ b/caPutLogApp/caPutLogAs.c @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -68,9 +69,15 @@ static void *logDataFreeList = 0; #define FREE_LIST_SIZE 1000 static void caPutLogAs(asTrapWriteMessage * pmessage, int afterPut); +static void (*psendCallback)(LOGDATA *); +static void (*pstopCallback)() = NULL; -int caPutLogAsInit() + +int caPutLogAsInit(void (*sendCallback)(LOGDATA *), void (*stopCallback)()) { + psendCallback = sendCallback; + pstopCallback = stopCallback; + if (!asActive) { errlogSevPrintf(errlogFatal, "caPutLog: access security is disabled\n"); return caPutLogError; @@ -92,7 +99,9 @@ int caPutLogAsInit() void caPutLogAsStop() { - caPutLogTaskStop(); + if (pstopCallback != NULL) { + pstopCallback(); + } if (listenerId) { asTrapWriteUnregisterListener(listenerId); @@ -140,9 +149,12 @@ static void caPutLogAs(asTrapWriteMessage *pmessage, int afterPut) plogData->pfield = paddr->pfield; options = 0; - num_elm = 1; + num_elm = caPutLogMaxArraySize(plogData->type); status = dbGetField( paddr, plogData->type, &plogData->old_value, &options, &num_elm, 0); + plogData->old_log_size = num_elm; + plogData->old_size = caPutLogActualArraySize(paddr); + plogData->is_array = paddr->no_elements > 1 ? TRUE : FALSE; if (status) { errlogPrintf("caPutLog: dbGetField error=%ld\n", status); @@ -156,9 +168,13 @@ static void caPutLogAs(asTrapWriteMessage *pmessage, int afterPut) plogData = (LOGDATA *) pmessage->userPvt; options = DBR_TIME; - num_elm = 1; + num_elm = caPutLogMaxArraySize(plogData->type); status = dbGetField( paddr, plogData->type, &plogData->new_value, &options, &num_elm, 0); + plogData->new_log_size = num_elm; + plogData->new_size = caPutLogActualArraySize(paddr); + plogData->is_array = plogData->is_array || paddr->no_elements > 1 ? TRUE : FALSE; + if (status) { errlogPrintf("caPutLog: dbGetField error=%ld.\n", status); plogData->type = DBR_STRING; @@ -170,8 +186,52 @@ static void caPutLogAs(asTrapWriteMessage *pmessage, int afterPut) plogData->new_value.time.secPastEpoch = curTime.secPastEpoch; plogData->new_value.time.nsec = curTime.nsec; } - caPutLogTaskSend(plogData); + psendCallback(plogData); + } +} + +int caPutLogMaxArraySize(short type) +{ +#if !JSON_AND_ARRAYS_SUPPORTED + return 1; +#else + static int const arraySizeLookUpTable [] = { + MAX_ARRAY_SIZE_BYTES/MAX_STRING_SIZE, /* DBR_STRING */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt8), /* DBR_CHAR */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt8), /* DBR_UCHAR */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt16), /* DBR_SHORT */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt16), /* DBR_USHORT */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt32), /* DBR_LONG */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt32), /* DBR_ULONG */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt64), /* DBR_INT64 */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt64), /* DBR_UINT64 */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsFloat32), /* DBR_FLOAT */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsFloat64), /* DBR_DOUBLE */ + MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt16) /* DBR_ENUM */ + }; + + if (type >= DBR_STRING || type <= DBR_ENUM){ + return arraySizeLookUpTable[type]; + } else { + errlogSevPrintf(errlogMajor, "caPutLogAs: Array size for type %d can not be determind\n", type); + return 1; + } +#endif +} + +long caPutLogActualArraySize(dbAddr * paddr) +{ + rset *prset = dbGetRset(paddr); + long nActual; + long offset; + + if (paddr->no_elements > 1 && + prset->get_array_info) { + prset->get_array_info(paddr, &nActual, &offset); + } else { + nActual = paddr->no_elements; } + return nActual; } void caPutLogDataFree(LOGDATA *plogData) diff --git a/caPutLogApp/caPutLogAs.h b/caPutLogApp/caPutLogAs.h index c35263c..45f6d0e 100644 --- a/caPutLogApp/caPutLogAs.h +++ b/caPutLogApp/caPutLogAs.h @@ -2,6 +2,7 @@ #define INCcaPutLogAsh 1 #include +#include #include "caPutLogTask.h" @@ -9,11 +10,14 @@ extern "C" { #endif -epicsShareFunc int caPutLogAsInit(); +epicsShareFunc int caPutLogAsInit(void (*sendCallback)(LOGDATA *), void (*stopCallback)()); epicsShareFunc void caPutLogAsStop(); epicsShareFunc void caPutLogDataFree(LOGDATA *pLogData); epicsShareFunc LOGDATA* caPutLogDataCalloc(void); +epicsShareFunc int caPutLogMaxArraySize(short type); +epicsShareFunc long caPutLogActualArraySize(dbAddr * paddr); + #ifdef __cplusplus } #endif diff --git a/caPutLogApp/caPutLogClient.c b/caPutLogApp/caPutLogClient.c index 9b326eb..ec81ef2 100644 --- a/caPutLogApp/caPutLogClient.c +++ b/caPutLogApp/caPutLogClient.c @@ -75,6 +75,10 @@ int caPutLogClientInit (const char *addr_str) if (!addr_str || !addr_str[0]) { addr_str = envGetConfigParamPtr(&EPICS_CA_PUT_LOG_ADDR); } + if (addr_str == NULL) { + errlogSevPrintf(errlogMajor, "caPutLog: server address not specified\n"); + return caPutLogError; + } status = aToIPAddr (addr_str, default_port, &saddr); if (status<0) { diff --git a/caPutLogApp/caPutLogTask.h b/caPutLogApp/caPutLogTask.h index ae005cf..45b6c41 100644 --- a/caPutLogApp/caPutLogTask.h +++ b/caPutLogApp/caPutLogTask.h @@ -12,6 +12,12 @@ extern "C" { #define MAX_USERID_SIZE 32 #define MAX_HOSTID_SIZE 32 +#if JSON_AND_ARRAYS_SUPPORTED +#define MAX_ARRAY_SIZE_BYTES 400 +#else +#define MAX_ARRAY_SIZE_BYTES 0 +#endif + typedef union { epicsInt8 v_int8; epicsUInt8 v_uint8; @@ -26,6 +32,20 @@ typedef union { epicsFloat32 v_float; epicsFloat64 v_double; char v_string[MAX_STRING_SIZE]; + +#if JSON_AND_ARRAYS_SUPPORTED + epicsInt8 a_int8[MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt8)]; + epicsUInt8 a_uint8[MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt8)]; + epicsInt16 a_int16[MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt16)]; + epicsUInt16 a_uint16[MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt16)]; + epicsInt32 a_int32[MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt32)]; + epicsUInt32 a_uint32[MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt32)]; + epicsInt64 a_int64[MAX_ARRAY_SIZE_BYTES/sizeof(epicsInt64)]; + epicsUInt64 a_uint64[MAX_ARRAY_SIZE_BYTES/sizeof(epicsUInt64)]; + epicsFloat32 a_float[MAX_ARRAY_SIZE_BYTES/sizeof(epicsFloat32)]; + epicsFloat64 a_double[MAX_ARRAY_SIZE_BYTES/sizeof(epicsFloat64)]; + char a_string[MAX_ARRAY_SIZE_BYTES/MAX_STRING_SIZE][MAX_STRING_SIZE]; +#endif } VALUE; typedef struct { @@ -39,6 +59,11 @@ typedef struct { TS_STAMP time; VALUE value; } new_value; + int is_array; + int old_size; + int old_log_size; + int new_size; + int new_log_size; } LOGDATA; epicsShareFunc int caPutLogTaskStart(int config); diff --git a/docs/index.rst b/docs/index.rst index f9c5b93..72295b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,38 +16,51 @@ same steps as for the regular iocLogServer, except that you probably want to use a different port. On the IOC side there are three routines to be called from the iocShell. See section `Setup`_ below for details. +This module will send logs of CA puts in a text format to a remote logging +server. Currently, supports two output formats, ``standard`` and ``JSON``. +The Json format is only supported with `EPICS`_ base versions 7.0.1+. + Download -------- -You can download a release from the links in the table below, or get the -latest development version via `darcs`_:: +You can download a release from the `Github releases`_:: page or get the +latest development version via `Github`_:: - darcs get http://www-csr.bessy.de/control/SoftDist/caPutLog/repo/caPutLog + git clone https://github.com/epics-modules/caPutLog.git You can also `browse`_ through the latest changes in the repo. -+---------+---------------+-----------------------------------+---------------+ -| Version | EPICS Release | Filename | Release Notes | -+=========+===============+===================================+===============+ -| 3.5 | 3.14.12..3.15 | :download:`caPutLog-3.5.tar.gz` | :ref:`R3-5` | -+---------+---------------+-----------------------------------+---------------+ -| 3.4 | 3.14.12 | :download:`caPutLog-3.4.tar.gz` | :ref:`R3-4` | -+---------+---------------+-----------------------------------+---------------+ -| 3.3.3 | 3.14.12 | :download:`caPutLog-3.3.3.tar.gz` | :ref:`R3-3-3` | -+---------+---------------+-----------------------------------+---------------+ -| 3.3.2 | 3.14.12 | :download:`caPutLog-3.3.2.tar.gz` | :ref:`R3-3-2` | -+---------+---------------+-----------------------------------+---------------+ -| 3.3.1 | 3.14.12 | :download:`caPutLog-3.3.1.tar.gz` | :ref:`R3-3-1` | -+---------+---------------+-----------------------------------+---------------+ -| 3.3 | 3.14.12 | :download:`caPutLog-3.3.tar.gz` | :ref:`R3-3` | -+---------+---------------+-----------------------------------+---------------+ -| 3.2 | 3.14.11 | :download:`caPutLog-3.2.tar.gz` | :ref:`R3-2` | -+---------+---------------+-----------------------------------+---------------+ -| 3.1 | 3.14.8.2 | :download:`caPutLog-3.1.tar.gz` | :ref:`R3-1` | -+---------+---------------+-----------------------------------+---------------+ -| 3.0 | 3.14.8.2 | :download:`caPutLog-3.0.tar.gz` | n/a | -+---------+---------------+-----------------------------------+---------------+ +Compatibility matrix and release notes +++++++++++++++++++++++++++++++++++++++ + ++---------+------------------+------------------+ +| Version | EPICS Release | Release Notes | ++=========+==================+==================+ +| 4.0 | 3.14.12..7 | :ref:`R4-0` | ++---------+------------------+------------------+ +| 3.7 | | | ++---------+------------------+------------------+ +| 3.6 | | | ++---------+------------------+------------------+ +| 3.5 | 3.14.12..3.15 | :ref:`R3-5` | ++---------+------------------+------------------+ +| 3.4 | 3.14.12 | :ref:`R3-4` | ++---------+------------------+------------------+ +| 3.3.3 | 3.14.12 | :ref:`R3-3-3` | ++---------+------------------+------------------+ +| 3.3.2 | 3.14.12 | :ref:`R3-3-2` | ++---------+------------------+------------------+ +| 3.3.1 | 3.14.12 | :ref:`R3-3-1` | ++---------+------------------+------------------+ +| 3.3 | 3.14.12 | :ref:`R3-3` | ++---------+------------------+------------------+ +| 3.2 | 3.14.11 | :ref:`R3-2` | ++---------+------------------+------------------+ +| 3.1 | 3.14.8.2 | :ref:`R3-1` | ++---------+------------------+------------------+ +| 3.0 | 3.14.8.2 | | ++---------+------------------+------------------+ Setup @@ -57,29 +70,48 @@ Build +++++ Change the definition of ``EPICS_BASE`` in ``configure/RELEASE`` according to -the location of epics base on the host, then (gnu-)make. Add the install -directory to your IOC application's ``configure/RELEASE``, and in the -Makefile add ``caPutLog.dbd`` to your dbd includes and ``caPutLog`` to the -libraries to link. +the location of epics base on the host, then (gnu-)make. + +Include to your EPICS application ++++++++++++++++++++++++++++++++++ + +To include the module to your IOC application, add the install directory to your +application's ``configure/RELEASE``, and include ``dbd`` and ``lib`` in the +``src`` Makefile: :: + + _DBD += caPutLog.dbd # For standard format + _DBD += caPutJsonLog.dbd # For JSON format (Exists only if module is compiled with supported version of base) + _LIBS += caPutLog # Required for both output formats + +for libraries to link. Configure +++++++++ -In your IOC startup file add the command:: +.. note:: Only one instance of the logger is supported to run at the time. + +In your IOC startup file add the following command for the standard output format:: caPutLogInit "host[:port]" [config] +or for JSON output format:: + + caPutJsonLogInit "host[:port]" [config] + where ``host`` (mandatory argument) is the IP address or host name of the log -server and ``port`` is optional (the default is 7011). The environment -variable ``EPICS_CA_PUT_LOG_ADDR`` is used if the first parameter to -``caPutLogInit`` is ``NULL`` or the empty string. +server and ``port`` is optional (the default is 7011). + +The environment variable ``EPICS_CA_PUT_LOG_ADDR`` / ``EPICS_CA_PUT_JSON_LOG_ADDR`` +is used if the first parameter to ``caPutLogInit`` / ``caPutJsonLogInit`` is ``NULL`` +or the empty string, respectively. + +The second (optional, default=0) argument should be one of: -The second (optional, default=0) argument should be one of :: +- ``-1`` - No logging (disabled) +- ``0`` - Log only on value change (ignore if old and new values are equal) +- ``1`` - Log all puts with burst filter +- ``2`` - Log all puts without any filters - #define caPutLogNone -1 /* no logging (disable) */ - #define caPutLogOnChange 0 /* log only on value change */ - #define caPutLogAll 1 /* log all puts */ - #define caPutLogAllNoFilter 2 /* log all puts no filtering on same PV*/ Make sure access security is enabled on the IOC by providing a suitable configuration file and load it with a call to @@ -93,17 +125,16 @@ unrestricted access):: RULE(1,WRITE,TRAPWRITE) } +.. note:: ``caPutLogInit`` or ``caPutJsonLogInit`` are expecting access security + to be already running, so they must be called *after* iocInit. -Note that ``caPutLogInit`` expects access security to be already running, so -must be called *after* iocInit. +Other shell commands for logger are: -Other shell commands are: - -``caPutLogReconf config`` +``caPutLogReconf config`` / ``caPutJsonLogReconf config`` Change configuration on-line. The argument is the same as in - ``caPutLogInit``. + ``caPutLogInit`` / ``caPutJsonLogInit``. -``caPutLogShow level`` +``caPutLogShow level`` / ``caPutJsonLogShow level`` Show information about a running caPutLog, level is the usual interest level (0, 1, or 2). @@ -116,8 +147,8 @@ though. However, you can also use the same log server instance (so that caput log messages and regular IOC log messages go into the same log file). -Log Format ----------- +Standard Log Format ++++++++++++++++++++ The iocLogServer precedes each line with these data:: @@ -140,12 +171,95 @@ or :: The latter format means that several puts for the same PV have been received in rapid succession; in this case only the original and the final value as well as the minimum and maximum value are logged. This filtering can be -disabled by specifying the ``caPutLogAllNoFilter`` configuration option. +disabled by specifying the ``caPutLogAllNoFilter`` (``2``) configuration option. + + +Json Log Format ++++++++++++++++ + +``caPutJsonLogger`` is using Json as the output format. General format looks like :: + + {"date":"","time":"