From d5f2de5a65354638e6423eee11f2d65a2edfe817 Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Mon, 1 Jun 2026 16:41:49 +0200 Subject: [PATCH 01/10] add logger backends mechanism --- .../logger_backends/sc_logger_broker.lua | 57 +++++++++++++++ .../logger_backends/sc_logger_file.lua | 70 +++++++++++++++++++ .../sc_logger.lua | 58 ++++----------- 3 files changed, 139 insertions(+), 46 deletions(-) create mode 100644 modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua create mode 100644 modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua diff --git a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua new file mode 100644 index 00000000..2f840d52 --- /dev/null +++ b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua @@ -0,0 +1,57 @@ +#!/bin/lua + +--- +-- logger that is using centreon broker methods to log +-- @module sc_logger_broker +-- @module ScLoggerBroker + +local sc_logger_broker = {} +local ScLoggerBroker = {} + +function sc_logger_broker.new(logger_params) + local self = {} + + self.severity = logger_params.severity + + if type(severity) ~= "number" then + self.severity = 1 + end + + self.logfile = logger_params.logfile or "/var/log/centreon-broker/stream-connector.log" + broker_log:set_parameters(self.severity, self.logfile) + + setmetatable(self, { __index = ScLoggerBroker}) + return self +end + +--- error: write an error message +-- @param message (string) the message that will be written +function ScLoggerBroker:error(message) + broker_log:error(1, message) +end + +--- warning: write a warning message +-- @param message (string) the message that will be written +function ScLoggerBroker:warning(message) + broker_log:warning(2, message) +end + +--- notice: write a notice message +-- @param message (string) the message that will be written +function ScLoggerBroker:notice(message) + broker_log:info(1, message) +end + +-- info: write an informational message +-- @param message (string) the message that will be written +function ScLoggerBroker:info(message) + broker_log:info(2,message) +end + +--- debug: write a debug message +-- @param message (string) the message that will be written +function ScLoggerBroker:debug(message) + broker_log:info(3, message) +end + +return sc_logger_broker \ No newline at end of file diff --git a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua new file mode 100644 index 00000000..b328e723 --- /dev/null +++ b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua @@ -0,0 +1,70 @@ +--- +-- logger that is using a text file to write logs +-- (main difference with the broker logger backend is that it is not using broker method as proxy to write into a file) +-- @module sc_logger_file +-- @module ScLoggerFile + +local sc_logger_file = {} +local ScLoggerFile = {} + +function sc_logger_file.new(logger_params) + local self = {} + + self.severity = logger_params.severity + + if type(severity) ~= "number" then + self.severity = 1 + end + + self.logfile = logger_params.logfile or "/var/log/centreon-broker/stream-connector.log" + self.fh = io.open(self.logfile, "a") + + setmetatable(self, { __index = ScLoggerFile}) + return self +end + +--- write_message: write a message in a file +-- @param message (string) the message to write +function ScLoggerFile:write_message(message) + local date = os.date("%a %b %d %H:%M:%S %Y") + io.write("[" .. date .. "]" .. message) +end + +--- error: write an error message +-- @param message (string) the message that will be written +function ScLoggerFile:error(message) + self:write_message("[ERROR] " .. message) +end + +--- warning: write a warning message +-- @param message (string) the message that will be written +function ScLoggerFile:warning(message) + if self.severity >= 2 then + self:write_message("[WARNING] " .. message) + end +end + +--- notice: write a notice message +-- @param message (string) the message that will be written +function ScLoggerFile:notice(message) + self:write_message("[NOTICE] " .. message) +end + +--- info: write an info message +-- @param message (string) the message that will be written +function ScLoggerFile:info(message) + if self.severity >= 2 then + self:write_message("[INFO] " .. message) + end +end + +--- debug: write an debug message +-- @param message (string) the message that will be written +function ScLoggerFile:debug(message) + if self.severity >= 3 then + self:write_message("[DEBUG] " .. message) + end +end + + +return sc_logger_file \ No newline at end of file diff --git a/modules/centreon-stream-connectors-lib/sc_logger.lua b/modules/centreon-stream-connectors-lib/sc_logger.lua index e5026530..eccd2136 100644 --- a/modules/centreon-stream-connectors-lib/sc_logger.lua +++ b/modules/centreon-stream-connectors-lib/sc_logger.lua @@ -6,86 +6,52 @@ -- @alias sc_logger local sc_logger = {} - ---- build_message: prepare log message --- @param severity (string) the severity of the message (WARNING, CRITIAL...) --- @param message (string) the log message --- @return ouput (string) the formated log message -local function build_message(severity, message) - local date = os.date("%a %b %d %H:%M:%S %Y") - local output = date .. ": " .. severity .. ": " .. message .. "\n" - - return output -end - - ---- write_message: write a message in a file --- @param message (string) the message to write --- @param logfile (string) the file in which the message will be written -local function write_message(message, logfile) - local file = io.open(logfile, "a") - io.output(file) - io.write(message) - io.close(file) -end - ---- file_logging: log message in a file --- @param message (string) the message that need to be written --- @param severity (string) the severity of the log --- @param logfile (string) the ouput file -local function file_logging(message, severity, logfile) - write_message(build_message(severity, message), logfile) -end - local ScLogger = {} --- sc_logger.new: sc_logger constructor --- @param [opt] logfile (string) output file for logs --- @param [opt] severity (integer) the accepted severity level -function sc_logger.new(logfile, severity) +-- @param [opt] logger_params (table) a table of params that contains needed parameters for the given logger backend +function sc_logger.new(logger_params) local self = {} - self.severity = severity - if type(severity) ~= "number" then - self.severity = 1 + if pcall(require, "centreon-stream-connectors-lib.logger_backends.sc_logger_" .. logger_params.logger_backend) then + local logger_backend = require("centreon-stream-connectors-lib.logger_backends.sc_logger_" .. logger_params.logger_backend) + self.logger_backend = logger_backend.new(logger_params) + else + self.logger_backend = require("centreon-stream-connectors-lib.logger_backends.sc_logger_broker") end - self.logfile = logfile or "/var/log/centreon-broker/stream-connector.log" - broker_log:set_parameters(self.severity, self.logfile) - setmetatable(self, { __index = ScLogger }) - return self end --- error: write an error message -- @param message (string) the message that will be written function ScLogger:error(message) - broker_log:error(1, message) + self.logger_backend:error(1, message) end --- warning: write a warning message -- @param message (string) the message that will be written function ScLogger:warning(message) - broker_log:warning(2, message) + self.logger_backend:warning(2, message) end --- notice: write a notice message -- @param message (string) the message that will be written function ScLogger:notice(message) - broker_log:info(1, message) + self.logger_backend:info(1, message) end -- info: write an informational message -- @param message (string) the message that will be written function ScLogger:info(message) - broker_log:info(2,message) + self.logger_backend:info(2,message) end --- debug: write a debug message -- @param message (string) the message that will be written function ScLogger:debug(message) - broker_log:info(3, message) + self.logger_backend:info(3, message) end --- log_curl_command: build a shell curl command based on given parameters and write it in the logfile From 91d19be8996dc9d8be2a54ce10cfcba5cacdb07d Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Wed, 3 Jun 2026 10:27:54 +0200 Subject: [PATCH 02/10] fix logger backend system --- .../logger_backends/sc_logger_file.lua | 4 ++-- modules/centreon-stream-connectors-lib/sc_logger.lua | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua index b328e723..91f3504d 100644 --- a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua +++ b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua @@ -17,7 +17,7 @@ function sc_logger_file.new(logger_params) end self.logfile = logger_params.logfile or "/var/log/centreon-broker/stream-connector.log" - self.fh = io.open(self.logfile, "a") + self.fh = io.open(self.logfile, "a+") setmetatable(self, { __index = ScLoggerFile}) return self @@ -27,7 +27,7 @@ end -- @param message (string) the message to write function ScLoggerFile:write_message(message) local date = os.date("%a %b %d %H:%M:%S %Y") - io.write("[" .. date .. "]" .. message) + self.fh:write("[" .. date .. "]" .. message .. "\n") end --- error: write an error message diff --git a/modules/centreon-stream-connectors-lib/sc_logger.lua b/modules/centreon-stream-connectors-lib/sc_logger.lua index eccd2136..3216f04a 100644 --- a/modules/centreon-stream-connectors-lib/sc_logger.lua +++ b/modules/centreon-stream-connectors-lib/sc_logger.lua @@ -27,31 +27,31 @@ end --- error: write an error message -- @param message (string) the message that will be written function ScLogger:error(message) - self.logger_backend:error(1, message) + self.logger_backend:error(message) end --- warning: write a warning message -- @param message (string) the message that will be written function ScLogger:warning(message) - self.logger_backend:warning(2, message) + self.logger_backend:warning(message) end --- notice: write a notice message -- @param message (string) the message that will be written function ScLogger:notice(message) - self.logger_backend:info(1, message) + self.logger_backend:notice(message) end -- info: write an informational message -- @param message (string) the message that will be written function ScLogger:info(message) - self.logger_backend:info(2,message) + self.logger_backend:info(message) end --- debug: write a debug message -- @param message (string) the message that will be written function ScLogger:debug(message) - self.logger_backend:info(3, message) + self.logger_backend:debug(message) end --- log_curl_command: build a shell curl command based on given parameters and write it in the logfile From 1a51a04cb77aa39374144c4bbd61aa9895a2655c Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Wed, 3 Jun 2026 14:53:32 +0200 Subject: [PATCH 03/10] add webserver --- .../logger_backends/sc_logger_file.lua | 1 + .../sc_webserver.lua | 269 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 modules/centreon-stream-connectors-lib/sc_webserver.lua diff --git a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua index 91f3504d..aa6084ef 100644 --- a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua +++ b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua @@ -28,6 +28,7 @@ end function ScLoggerFile:write_message(message) local date = os.date("%a %b %d %H:%M:%S %Y") self.fh:write("[" .. date .. "]" .. message .. "\n") + self.fh:flush() end --- error: write an error message diff --git a/modules/centreon-stream-connectors-lib/sc_webserver.lua b/modules/centreon-stream-connectors-lib/sc_webserver.lua new file mode 100644 index 00000000..0315b107 --- /dev/null +++ b/modules/centreon-stream-connectors-lib/sc_webserver.lua @@ -0,0 +1,269 @@ +#!/usr/bin/lua + +--- +-- HTTP server module for centreon stream connectors standalone mode +-- Handles incoming GET and POST requests and routes them to registered handlers. +-- Depends on luasocket (require "socket"). +-- @module sc_webserver +-- @alias sc_webserver + +local sc_webserver = {} +local ScWebserver = {} + +local socket = require("socket") + +--- sc_webserver.new: sc_webserver constructor +-- @param params (table) configuration table +-- @param sc_logger (table) instance of the sc_logger module +-- @param sc_common (table) instance of the sc_common module +function sc_webserver.new(params, sc_logger, sc_common) + local self = {} + + self.params = params + self.params.webserver_port = self.params.webserver_port or 8086 + self.params.webserver_listen_address = self.params.webserver_listen_address or "127.0.0.1" + self.sc_logger = sc_logger + self.sc_common = sc_common + self.server = nil + self.routes = { + GET = {}, + POST = {} + } + + setmetatable(self, { __index = ScWebserver }) + return self +end + +--- add_get_route: register a handler for GET requests on a given path +-- @param path (string) URL path to match (e.g. "/events") +-- @param handler (function) called as handler(request) and must return a response table: +-- { status (number), status_text (string), body (string), content_type (string) } +function ScWebserver:add_get_route(path, handler) + self.routes.GET[path] = handler +end + +--- add_post_route: register a handler for POST requests on a given path +-- @param path (string) URL path to match (e.g. "/events") +-- @param handler (function) called as handler(request) and must return a response table: +-- { status (number), status_text (string), body (string), content_type (string) } +function ScWebserver:add_post_route(path, handler) + self.routes.POST[path] = handler +end + +--- start: bind the socket and begin listening +-- Must be called before process() or run(). +-- @return true on success, nil + error string on failure +function ScWebserver:start() + local srv = socket.tcp() + srv:setoption("reuseaddr", true) + + local ok, bind_err = srv:bind(self.params.webserver_listen_address, self.params.webserver_port) + local try = 1 + local err + + -- address may still be bound from previous execution. We do 10 retry before giving up + while not ok and try < 10 do + err = "sc_webserver: failed to bind to " .. self.params.webserver_listen_address .. ":" .. tostring(self.params.webserver_port) .. " - " .. tostring(bind_err) + self.sc_logger:error("[sc_webserver:start]: " .. err) + self.sc_common:sleep(1) + ok, bind_err = srv:bind(self.params.webserver_listen_address, self.params.webserver_port) + try = try + 1 + end + + if not ok then + err = "sc_webserver: failed to bind to " .. self.params.webserver_listen_address .. ":" .. tostring(self.params.webserver_port) .. " - " .. tostring(bind_err) + self.sc_logger:error("[sc_webserver:start]: " .. err) + srv:close() + return nil, err + end + + local listen_ok, listen_err = srv:listen(10) + if not listen_ok then + err = "sc_webserver: failed to listen - " .. tostring(listen_err) + self.sc_logger:error("[sc_webserver:start]: " .. err) + srv:close() + return nil, err + end + + -- non-blocking accept so process() can be used in an external loop + srv:settimeout(0) + self.server = srv + self.sc_logger:notice("[sc_webserver:start]: listening on " .. self.params.webserver_listen_address .. ":" .. tostring(self.params.webserver_port)) + return true +end + +--- parse_request: read and parse an HTTP request from a connected client socket +-- @param client (socket) connected TCP client +-- @return request table on success, nil + error string on failure +-- Request table fields: +-- method (string), path (string), query_string (string), +-- http_version (string), headers (table), body (string) +function ScWebserver:parse_request(client) + local line, err = client:receive("*l") + if not line then + return nil, "failed to read request line: " .. tostring(err) + end + + -- strip trailing CR if present (luasocket strips LF but not CR) + line = line:gsub("\r$", "") + + local method, raw_path, http_version = line:match("^(%u+) (%S+) HTTP/(%S+)$") + if not method then + return nil, "malformed request line: " .. tostring(line) + end + + local path, query_string = raw_path:match("^([^?]*)%??(.*)") + + local headers = {} + local content_length = 0 + while true do + local hline, herr = client:receive("*l") + if not hline or hline == "" or hline == "\r" then + break + end + hline = hline:gsub("\r$", "") + local name, value = hline:match("^([^:]+):%s*(.-)%s*$") + if name then + local lower_name = name:lower() + headers[lower_name] = value + if lower_name == "content-length" then + content_length = tonumber(value) or 0 + end + end + end + + local body = "" + if content_length > 0 then + local received, recv_err = client:receive(content_length) + body = received or "" + end + + return { + method = method, + path = path, + query_string = query_string, + http_version = http_version, + headers = headers, + body = body + } +end + +--- send_response: write an HTTP response to a client socket +-- @param client (socket) connected TCP client +-- @param status_code (number) HTTP status code +-- @param status_text (string) HTTP status text +-- @param body (string) response body +-- @param content_type (string) Content-Type header value (default: "application/json") +function ScWebserver:send_response(client, status_code, status_text, body, content_type) + content_type = content_type or "application/json" + body = body or "" + local response = table.concat({ + "HTTP/1.1 " .. tostring(status_code) .. " " .. tostring(status_text), + "Content-Type: " .. content_type, + "Content-Length: " .. tostring(#body), + "Connection: close", + "", + body + }, "\r\n") + client:send(response) +end + +--- handle_connection: parse one HTTP request and dispatch it to the matching route handler +-- @param client (socket) connected TCP client +function ScWebserver:handle_connection(client) + client:settimeout(5) + + local request, parse_err = self:parse_request(client) + + if not request then + self.sc_logger:warning("[sc_webserver:handle_connection]: failed to parse request: " .. tostring(parse_err)) + self:send_response(client, 400, "Bad Request", '{"error":"bad request"}') + return + end + + self.sc_logger:debug("[sc_webserver:handle_connection]: " .. tostring(request.method) .. " " .. tostring(request.path)) + + if request.method ~= "GET" and request.method ~= "POST" then + self.sc_logger:warning("[sc_webserver:handle_connection]: method not allowed. Received method: " .. tostring(parse_err)) + self:send_response(client, 405, "Method Not Allowed", '{"error":"method not allowed"}') + return + end + + local handler = self.routes[request.method][request.path] + if not handler then + self.sc_logger:warning("[sc_webserver:handle_connection]: 404 not found. Path: " .. tostring(request.path)) + self:send_response(client, 404, "Not Found", '{"error":"not found"}') + return + end + + local ok, result = pcall(handler, request) + if not ok then + self.sc_logger:error("[sc_webserver:handle_connection]: Internal server error: " .. tostring(result)) + self:send_response(client, 500, "Internal Server Error", '{"error":"internal server error ' .. tostring(result) .. '"}') + return + end + + self:send_response( + client, + result.status or 200, + result.status_text or "OK", + result.body .. "\n" or "\n", + result.content_type or "application/json" + ) +end + +--- process: accept and handle one pending connection without blocking +-- Returns immediately when no connection is waiting. +-- Use this inside an external event loop (e.g. sc_standalone's main loop). +-- start() must be called before process(). +-- @return true on success, nil + error string if the server is not started +function ScWebserver:process() + if not self.server then + return nil, "sc_webserver: server not started, call start() first" + end + + local client = self.server:accept() + if client then + self:handle_connection(client) + client:close() + end + + return true +end + +--- run: start the server (if not already started) and block in an accept loop +-- Calls start() internally when needed. +-- @return nil + error string if startup fails; otherwise never returns +function ScWebserver:run() + if not self.server then + local ok, err = self:start() + if not ok then + return nil, err + end + end + + -- switch to a 1-second timeout so the loop can react to signals + self.server:settimeout(1) + self.sc_logger:notice("[sc_webserver:run]: entering run loop") + + while true do + local client, accept_err = self.server:accept() + if client then + self:handle_connection(client) + client:close() + elseif accept_err ~= "timeout" then + self.sc_logger:error("[sc_webserver:run]: accept error: " .. tostring(accept_err)) + end + end +end + +--- stop: close the listening socket +function ScWebserver:stop() + if self.server then + self.server:close() + self.server = nil + self.sc_logger:notice("[sc_webserver:stop]: server stopped") + end +end + +return sc_webserver From f45a882559ad83e98196ef24ac61aa36fad0056c Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Wed, 3 Jun 2026 16:36:43 +0200 Subject: [PATCH 04/10] add line return in response --- modules/centreon-stream-connectors-lib/sc_webserver.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/centreon-stream-connectors-lib/sc_webserver.lua b/modules/centreon-stream-connectors-lib/sc_webserver.lua index 0315b107..530a6910 100644 --- a/modules/centreon-stream-connectors-lib/sc_webserver.lua +++ b/modules/centreon-stream-connectors-lib/sc_webserver.lua @@ -156,7 +156,7 @@ end -- @param content_type (string) Content-Type header value (default: "application/json") function ScWebserver:send_response(client, status_code, status_text, body, content_type) content_type = content_type or "application/json" - body = body or "" + body = body .. "\n" or "\n" local response = table.concat({ "HTTP/1.1 " .. tostring(status_code) .. " " .. tostring(status_text), "Content-Type: " .. content_type, From 65fb46729358a98688c89df97da3dd5fd3198154 Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Mon, 8 Jun 2026 11:28:48 +0200 Subject: [PATCH 05/10] fix log level --- .../logger_backends/sc_logger_broker.lua | 8 ++++---- .../logger_backends/sc_logger_file.lua | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua index 2f840d52..eb1ef3a1 100644 --- a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua +++ b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_broker.lua @@ -11,14 +11,14 @@ local ScLoggerBroker = {} function sc_logger_broker.new(logger_params) local self = {} - self.severity = logger_params.severity + self.log_level = logger_params.log_level - if type(severity) ~= "number" then - self.severity = 1 + if type(self.log_level) ~= "number" then + self.log_level = 1 end self.logfile = logger_params.logfile or "/var/log/centreon-broker/stream-connector.log" - broker_log:set_parameters(self.severity, self.logfile) + broker_log:set_parameters(self.log_level, self.logfile) setmetatable(self, { __index = ScLoggerBroker}) return self diff --git a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua index aa6084ef..937ed35d 100644 --- a/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua +++ b/modules/centreon-stream-connectors-lib/logger_backends/sc_logger_file.lua @@ -10,10 +10,10 @@ local ScLoggerFile = {} function sc_logger_file.new(logger_params) local self = {} - self.severity = logger_params.severity + self.log_level = logger_params.log_level - if type(severity) ~= "number" then - self.severity = 1 + if type(self.log_level) ~= "number" then + self.log_level = 1 end self.logfile = logger_params.logfile or "/var/log/centreon-broker/stream-connector.log" @@ -40,7 +40,7 @@ end --- warning: write a warning message -- @param message (string) the message that will be written function ScLoggerFile:warning(message) - if self.severity >= 2 then + if self.log_level >= 2 then self:write_message("[WARNING] " .. message) end end @@ -54,7 +54,7 @@ end --- info: write an info message -- @param message (string) the message that will be written function ScLoggerFile:info(message) - if self.severity >= 2 then + if self.log_level >= 2 then self:write_message("[INFO] " .. message) end end @@ -62,7 +62,7 @@ end --- debug: write an debug message -- @param message (string) the message that will be written function ScLoggerFile:debug(message) - if self.severity >= 3 then + if self.log_level >= 3 then self:write_message("[DEBUG] " .. message) end end From 626872ee137e694782c4465c7316c279306b63ce Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Tue, 9 Jun 2026 11:12:27 +0200 Subject: [PATCH 06/10] add new execution_mode and logger backend params --- .../sc_event.lua | 63 +++++++++++++++---- .../sc_params.lua | 2 + 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/modules/centreon-stream-connectors-lib/sc_event.lua b/modules/centreon-stream-connectors-lib/sc_event.lua index 48ccee2f..d33456de 100644 --- a/modules/centreon-stream-connectors-lib/sc_event.lua +++ b/modules/centreon-stream-connectors-lib/sc_event.lua @@ -39,6 +39,11 @@ function sc_event.new(broker_event, params, common, logger, broker, storage) local event_meta = { __index = function (tbl, key) return self.broker_event[key] end} setmetatable(self.event, event_meta) + -- in standalone mode, a cache table is supposed to be shipped with the event. We put it in self.event "table" + if self.broker_event.cache and self.params.execution_mode == "standalone" then + self.event.cache = self.broker_event.cache + end + self.validation_steps = {} for accepted_element, info in pairs(self.params.accepted_elements_info) do @@ -520,7 +525,11 @@ function ScEvent:is_valid_host() return false end - self.event.cache.host = self.sc_broker:get_host_all_infos(self.event.host_id) + if self.params.execution_mode == "standalone" then + self.event.cache.host = self.event.cache.host + else + self.event.cache.host = self.sc_broker:get_host_all_infos(self.event.host_id) + end -- return false if we can't get hostname if (not self.event.cache.host and self.params.skip_anon_events == 1) then @@ -579,7 +588,11 @@ function ScEvent:is_valid_service() return false end - self.event.cache.service = self.sc_broker:get_service_all_infos(self.event.host_id, self.event.service_id) + if self.params.execution_mode == "standalone" then + self.event.cache.service = self.event.cache.service + else + self.event.cache.service = self.sc_broker:get_service_all_infos(self.event.host_id, self.event.service_id) + end -- return false if we can't get service description if (not self.event.cache.service and self.params.skip_anon_events == 1) then @@ -765,7 +778,11 @@ end --- is_valid_hostgroup: check if the event is in an accepted hostgroup -- @return true|false (boolean) function ScEvent:is_valid_hostgroup() - self.event.cache.hostgroups = self.sc_broker:get_hostgroups(self.event.host_id) + if self.params.execution_mode == "standalone" then + self.event.cache.hostgroups = self.event.cache.hostgroups + else + self.event.cache.hostgroups = self.sc_broker:get_hostgroups(self.event.host_id) + end -- return true if options are not set or if both options are set local accepted_hostgroups_isnotempty = self.params.accepted_hostgroups ~= "" @@ -834,7 +851,11 @@ end --- is_valid_servicegroup: check if the event is in an accepted servicegroup -- @return true|false (boolean) function ScEvent:is_valid_servicegroup() - self.event.cache.servicegroups = self.sc_broker:get_servicegroups(self.event.host_id, self.event.service_id) + if self.params.execution_mode == "standalone" then + self.event.cache.servicegroups = self.event.cache.servicegroups + else + self.event.cache.servicegroups = self.sc_broker:get_servicegroups(self.event.host_id, self.event.service_id) + end -- return true if options are not set or if both options are set local accepted_servicegroups_isnotempty = self.params.accepted_servicegroups ~= "" @@ -1013,7 +1034,11 @@ function ScEvent:is_valid_ba() return false end - self.event.cache.ba = self.sc_broker:get_ba_infos(self.event.ba_id) + if self.params.execution_mode == "standalone" then + self.event.cache.ba = self.event.cache.ba + else + self.event.cache.ba = self.sc_broker:get_ba_infos(self.event.ba_id) + end -- return false if we can't get ba name if (not self.event.cache.ba.ba_name and self.params.skip_anon_events == 1) then @@ -1066,7 +1091,11 @@ end --- is_valid_bv: check if the event is in an accepted BV -- @return true|false (boolean) function ScEvent:is_valid_bv() - self.event.cache.bvs = self.sc_broker:get_bvs_infos(self.event.host_id) + if self.params.execution_mode == "standalone" then + self.event.cache.bvs = self.event.cache.bvs + else + self.event.cache.bvs = self.sc_broker:get_bvs_infos(self.event.host_id) + end -- return true if options are not set or if both options are set local accepted_bvs_isnotempty = self.params.accepted_bvs ~= "" @@ -1136,7 +1165,11 @@ function ScEvent:is_valid_poller() return false end - self.event.cache.poller = self.sc_broker:get_instance(self.event.cache.host.instance_id) + if self.params.execution_mode == "standalone" then + self.event.cache.poller = self.event.cache.poller + else + self.event.cache.poller = self.sc_broker:get_instance(self.event.cache.host.instance_id) + end -- required if we want to easily have access to poller name with macros {cache.instance.name} self.event.cache.instance = { @@ -1208,8 +1241,12 @@ function ScEvent:is_valid_host_severity() self.event.cache.severity = {} end - -- get severity of the host from broker cache - self.event.cache.severity.host = self.sc_broker:get_severity(self.event.host_id) + if self.params.execution_mode == "standalone" then + self.event.cache.severity.host = self.event.cache.severity.host + else + -- get severity of the host from broker cache + self.event.cache.severity.host = self.sc_broker:get_severity(self.event.host_id) + end -- return true if there is no severity filter if self.params.host_severity_threshold == nil then @@ -1236,8 +1273,12 @@ function ScEvent:is_valid_service_severity() self.event.cache.severity = {} end - -- get severity of the host from broker cache - self.event.cache.severity.service = self.sc_broker:get_severity(self.event.host_id, self.event.service_id) + if self.params.execution_mode == "standalone" then + self.event.cache.severity.service = self.event.cache.severity.service + else + -- get severity of the host from broker cache + self.event.cache.severity.service = self.sc_broker:get_severity(self.event.host_id, self.event.service_id) + end -- return true if there is no severity filter if self.params.service_severity_threshold == nil then diff --git a/modules/centreon-stream-connectors-lib/sc_params.lua b/modules/centreon-stream-connectors-lib/sc_params.lua index 7ab8d6e8..dfd34305 100644 --- a/modules/centreon-stream-connectors-lib/sc_params.lua +++ b/modules/centreon-stream-connectors-lib/sc_params.lua @@ -121,11 +121,13 @@ function sc_params.new(common, logger) -- testing parameters send_data_test = 0, + execution_mode = "standard", -- logging parameters logfile = "", log_level = "", log_curl_commands = 0, + logger_backend = "broker", -- storage parameters load_host_properties_from_storage = "", From ce072ef30a540cf141dbb3b2ee1db189ca686773 Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Tue, 9 Jun 2026 11:14:18 +0200 Subject: [PATCH 07/10] add standalone stream connector --- .../standalone/sc_standalone_daemon.lua | 162 +++++++++ .../standalone/standalone-events-apiv2.lua | 335 ++++++++++++++++++ .../sc_webserver.lua | 4 +- 3 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 centreon-certified/standalone/sc_standalone_daemon.lua create mode 100644 centreon-certified/standalone/standalone-events-apiv2.lua diff --git a/centreon-certified/standalone/sc_standalone_daemon.lua b/centreon-certified/standalone/sc_standalone_daemon.lua new file mode 100644 index 00000000..510e64dd --- /dev/null +++ b/centreon-certified/standalone/sc_standalone_daemon.lua @@ -0,0 +1,162 @@ +#!/bin/lua + +local getopt = require("centreon-stream-connectors-lib.sc_getopt") +local json = require("centreon-stream-connectors-lib.sc_json") +local sc_webserver = require("centreon-stream-connectors-lib.sc_webserver") + +local debug = false +local log_file = "/var/log/standalone_stream_connector.log" +local running_mode = "standalone" +local logger_backend = 'file' +local params + +local append = false +local nonoptions = {} +local infile = io.input() +local sc_file + +local usage = arg[-1] .. " " .. arg[0] .. [[ + You are using the stream connector standalone mode. This mode allow you to use stream connectors outside of the standard centreon-broker environment. + Options : + -s) the path to the stream connector that must be run (e.g: /usr/share/centreon-broker/lua/splunk-events-apiv2.lua) + -l) [optional] the log file that is going to be used by the stream connector (default: /var/log/standalone_stream_connector.log) + -d) [optional] when set, enable debug for the standalone script + -m) [optional] execution mode, can be "standalone" or "standard", default is "standalone" (you should probably not use the standard mode when running this tool) + -b) [optional] the logger backend that must be used (default: file) + -p) [optional] They option can be optional but usually each stream connector has its set of mandatory params. If there is at least one mandatory params then you must use this option. json encoded list of params for the stream connector e.g: -p '{"http_server_url":"https://mysplunk.test.loal","splunk_token":"abcdef","send_data_test":1}' +]] .. arg[-1] .. " " .. arg[0] .. " -s /usr/share/centreon-broker/lua/splunk-events-apiv2.lua [-l /var/log/standalone_stream_connector.log] [-d] [-m standalone]" + + +for opt, arg in getopt(arg, 's:l:m:b:p:d', nonoptions) do + if opt == 's' then + sc_file = arg + elseif opt == 'l' then + log_file = arg + elseif opt == 'd' then + debug = true + elseif opt == 'm' then + running_mode = arg + elseif opt == 'b' then + logger_backend = arg + elseif opt == 'p' then + params = arg + elseif opt == '?' then + print('[ERROR]: unknown option: ' .. tostring(arg) .. ".\n " .. usage) + os.exit(1) + elseif opt == ':' then + print('[ERROR]: missing argument: ' .. tostring(arg) .. ".\n " .. usage) + os.exit(1) + else + print('[ERROR]: unknown error: ' .. tostring(arg) .. ".\n " .. usage) + os.exit(1) + end +end + +-- useless ?? +if #nonoptions == 1 then + infile = io.open(nonoptions[1], 'r') +elseif #nonoptions > 1 then + print('[ERROR]: wrong number of arguments: ' .. tostring(arg) .. ".\n " .. usage) + os.exit(1) +end + +if not sc_file then + print('[ERROR]: no stream connector to load received. \n' .. usage) + os.exit(1) +end + +local stream_connector_init_params = { + logfile = log_file, + logger_backend = logger_backend, + running_mode = running_mode +} + +local stream_connector = assert(loadfile(sc_file)) + +if type(stream_connector) ~= "function" then + print(tostring(stream_connector)) + os.exit(1) +end + +stream_connector() + +sc_params = json:decode(params) +for param_name, param_value in pairs(sc_params) do + stream_connector_init_params[param_name] = param_value +end + +-- create a global broker variable that we will use to override some very basic function normally provided by centreon-broker +broker = { + bbdo_version = function () return '3.0.0' end, + json_encode = function (t) return json:encode(t) end, + json_decode = function (s) return json:decode(s) end +} + +-- needs to be global otherwise you'll get this kind of error "attempt to index a nil value (upvalue 'queue')" +queue = EventQueue.new(stream_connector_init_params) + +webserver = sc_webserver.new(queue.sc_params.params, queue.sc_logger, queue.sc_common) +local result, err = webserver:start() + +queue.ws_counter = { + counter = 1, + counter_func = function () + queue.ws_counter.counter = queue.ws_counter.counter + 1 + return { + status = 200, + status_text = "success", + body = '{"counter":' .. queue.ws_counter.counter .. '}', + content_type = "application/json" + } + end +} + +queue.endpoint_callbacks = { + events = function (http_data) + queue.sc_logger:debug("[sc_standalone:endpoint_callback]: http data: " .. queue.sc_common:dumper(http_data)) + local success, data = pcall(json.decode, json, http_data.body) + + -- list of events is not a valid json + if not success then + return { + status = 400, + status_text = "Bad Request", + body = '{"error":"' .. tostring(data) .. '"}', + content_type = "application/json" + } + end + + for index, event in ipairs(data) do + success, data = pcall(write, event) + + if not success then + return { + status = 500, + status_text = "Internal server error", + body = '{"error":"' .. tostring(data) .. '"}', + content_type = "application/json" + } + end + end + + return { + status = 200, + status_text = "OK", + body = '{"error":"","events":"' .. http_data.body .. '"}', + content_type = "application/json" + } + end +} + +webserver:add_post_route("/events", queue.endpoint_callbacks.events) + +if not result then + queue.sc_logger:error(err) + os.exit(1) +end + +-- webserver:stop() + +while true do + webserver:process() +end \ No newline at end of file diff --git a/centreon-certified/standalone/standalone-events-apiv2.lua b/centreon-certified/standalone/standalone-events-apiv2.lua new file mode 100644 index 00000000..5bb1e727 --- /dev/null +++ b/centreon-certified/standalone/standalone-events-apiv2.lua @@ -0,0 +1,335 @@ +#!/usr/bin/lua +-------------------------------------------------------------------------------- +-- Centreon Broker logstash Connector Events +-------------------------------------------------------------------------------- + + +-- Libraries +local curl = require "cURL" +local sc_common = require("centreon-stream-connectors-lib.sc_common") +local sc_logger = require("centreon-stream-connectors-lib.sc_logger") +local sc_broker = require("centreon-stream-connectors-lib.sc_broker") +local sc_event = require("centreon-stream-connectors-lib.sc_event") +local sc_params = require("centreon-stream-connectors-lib.sc_params") +local sc_macros = require("centreon-stream-connectors-lib.sc_macros") +local sc_flush = require("centreon-stream-connectors-lib.sc_flush") + +-------------------------------------------------------------------------------- +-- Classe event_queue +-------------------------------------------------------------------------------- + +local EventQueue = {} +EventQueue.__index = EventQueue + +-------------------------------------------------------------------------------- +---- Constructor +---- @param conf The table given by the init() function and returned from the GUI +---- @return the new EventQueue +---------------------------------------------------------------------------------- + +function EventQueue.new(params) + local self = {} + + local mandatory_parameters = {} + + self.fail = false + + -- set up log configuration + params.logfile = params.logfile or "/var/log/centreon-broker/standalone-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" + + + -- initiate mandatory objects + self.sc_logger = sc_logger.new(params) + self.sc_common = sc_common.new(self.sc_logger) + self.sc_params = sc_params.new(self.sc_common, self.sc_logger) + + -- checking mandatory parameters and setting a fail flag + if not self.sc_params:is_mandatory_config_set(mandatory_parameters, params) then + self.fail = true + end + + -- overriding default parameters for this stream connector if the default values doesn't suit the basic needs + self.sc_params.params.http_server_url = params.http_server_url or 'http://127.0.0.1' + self.sc_params.params.port = params.port or 8086 + self.sc_params.params.api_endpoint = params.api_endpoint or "/events" + self.sc_params.params.accepted_categories = params.accepted_categories or "neb" + self.sc_params.params.accepted_elements = params.accepted_elements or "host_status,service_status" + + -- apply users params and check syntax of standard ones + self.sc_params:param_override(params) + self.sc_params:check_params() + + self.sc_macros = sc_macros.new(self.sc_params.params, self.sc_logger) + self.format_template = self.sc_params:load_event_format_file() + + -- only load the custom code file, not executed yet + if self.sc_params.load_custom_code_file and not self.sc_params:load_custom_code_file(self.sc_params.params.custom_code_file) then + self.sc_logger:error("[EventQueue:new]: couldn't successfully load the custom code file: " .. tostring(self.sc_params.params.custom_code_file)) + end + + self.sc_params:build_accepted_elements_info() + self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) + + local categories = self.sc_params.params.bbdo.categories + local elements = self.sc_params.params.bbdo.elements + + self.format_event = { + [categories.neb.id] = { + [elements.host_status.id] = function () return self:format_event_host() end, + [elements.service_status.id] = function () return self:format_event_service() end + }, + [categories.bam.id] = {} + } + + self.send_data_method = { + [1] = function (payload, queue_metadata) return self:send_data(payload, queue_metadata) end + } + + self.build_payload_method = { + [1] = function (payload, event) return self:build_payload(payload, event) end + } + + -- return EventQueue object + setmetatable(self, { __index = EventQueue }) + return self +end + +-------------------------------------------------------------------------------- +---- EventQueue:format_event method +---------------------------------------------------------------------------------- +function EventQueue:format_accepted_event() + local category = self.sc_event.event.category + local element = self.sc_event.event.element + local template = self.sc_params.params.format_template[category][element] + self.sc_logger:debug("[EventQueue:format_event]: starting format event") + self.sc_event.event.formated_event = {} + + if self.format_template and template ~= nil and template ~= "" then + for index, value in pairs(template) do + self.sc_event.event.formated_event[index] = self.sc_macros:replace_sc_macro(value, self.sc_event.event) + end + else + -- can't format event if stream connector is not handling this kind of event and that it is not handled with a template file + if not self.format_event[category][element] then + self.sc_logger:error("[format_event]: You are trying to format an event with category: " + .. tostring(self.sc_params.params.reverse_category_mapping[category]) .. " and element: " + .. tostring(self.sc_params.params.reverse_element_mapping[category][element]) + .. ". If it is a not a misconfiguration, you should create a format file to handle this kind of element") + else + self.format_event[category][element]() + end + end + + self:add() + self.sc_logger:debug("[EventQueue:format_event]: event formatting is finished") +end + +function EventQueue:format_event_host() + -- nothing to do + return true +end + +function EventQueue:format_event_service() + -- nothing to do + return true +end + +-------------------------------------------------------------------------------- +-- EventQueue:add, add an event to the sending queue +-------------------------------------------------------------------------------- +function EventQueue:add() + -- store event in self.events lists + local category = self.sc_event.event.category + local element = self.sc_event.event.element + + self.sc_logger:debug("[EventQueue:add]: add event in queue category: " .. tostring(self.sc_params.params.reverse_category_mapping[category]) + .. " element: " .. tostring(self.sc_params.params.reverse_element_mapping[category][element])) + + self.sc_logger:debug("[EventQueue:add]: queue size before adding event: " .. tostring(#self.sc_flush.queues[category][element].events)) + self.sc_flush.queues[category][element].events[#self.sc_flush.queues[category][element].events + 1] = self.sc_event.event + + self.sc_logger:info("[EventQueue:add]: queue size is now: " .. tostring(#self.sc_flush.queues[category][element].events) + .. ", max is: " .. tostring(self.sc_params.params.max_buffer_size)) +end + +-------------------------------------------------------------------------------- +-- EventQueue:build_payload, concatenate data so it is ready to be sent +-- @param payload {string} json encoded string +-- @param event {table} the event that is going to be added to the payload +-- @return payload {string} json encoded string +-------------------------------------------------------------------------------- +function EventQueue:build_payload(payload, event) + if not payload then + payload = {event} + else + payload = table.insert(payload, event) + end + + return payload +end + +function EventQueue:send_data(payload, queue_metadata) + self.sc_logger:debug("[EventQueue:send_data]: Starting to send data") + + local url = self.sc_params.params.http_server_url .. ":" .. self.sc_params.params.port .. self.sc_params.params.api_endpoint + payload = broker.json_encode(payload) + queue_metadata.headers = {"content-type: application/json"} + queue_metadata.method = "POST" + self.sc_logger:log_curl_command(url, queue_metadata, self.sc_params.params, payload) + + -- write payload in the logfile for test purpose + if self.sc_params.params.send_data_test == 1 then + self.sc_logger:notice("[send_data]: " .. tostring(payload)) + return true + end + + self.sc_logger:info("[EventQueue:send_data]: Going to send the following json " .. tostring(payload)) + self.sc_logger:info("[EventQueue:send_data]: Logstash address is: " .. tostring(url)) + + local http_response_body = "" + local http_request = curl.easy() + :setopt_url(url) + :setopt_writefunction( + function (response) + http_response_body = http_response_body .. tostring(response) + end + ) + :setopt(curl.OPT_TIMEOUT, self.sc_params.params.connection_timeout) + :setopt(curl.OPT_SSL_VERIFYPEER, self.sc_params.params.verify_certificate) + :setopt(curl.OPT_CUSTOMREQUEST, queue_metadata.method) + :setopt(curl.OPT_HTTPHEADER, queue_metadata.headers) + + -- set proxy address configuration + if (self.sc_params.params.proxy_address ~= '') then + if (self.sc_params.params.proxy_port ~= '') then + http_request:setopt(curl.OPT_PROXY, self.sc_params.params.proxy_address .. ':' .. self.sc_params.params.proxy_port) + else + self.sc_logger:error("[EventQueue:send_data]: proxy_port parameter is not set but proxy_address is used") + end + end + + -- set proxy user configuration + if (self.sc_params.params.proxy_username ~= '') then + if (self.sc_params.params.proxy_password ~= '') then + http_request:setopt(curl.OPT_PROXYUSERPWD, self.sc_params.params.proxy_username .. ':' .. self.sc_params.params.proxy_password) + else + self.sc_logger:error("[EventQueue:send_data]: proxy_password parameter is not set but proxy_username is used") + end + end + + -- adding the HTTP POST data + http_request:setopt_postfields(payload) + + -- performing the HTTP request + http_request:perform() + + -- collecting results + http_response_code = http_request:getinfo(curl.INFO_RESPONSE_CODE) + + http_request:close() + + -- Handling the return code + local retval = false + if http_response_code == 200 then + self.sc_logger:info("[EventQueue:send_data]: HTTP POST request successful: return code is " .. tostring(http_response_code)) + retval = true + else + self.sc_logger:error("[EventQueue:send_data]: HTTP POST request FAILED, return code is " .. tostring(http_response_code) .. ". Message is: " .. tostring(http_response_body)) + + if payload then + self.sc_logger:error("[EventQueue:send_data]: sent payload was: " .. tostring(payload)) + end + end + + return retval +end + +-------------------------------------------------------------------------------- +-- Required functions for Broker StreamConnector +-------------------------------------------------------------------------------- + +local queue + +-- Fonction init() +function init(conf) + queue = EventQueue.new(conf) +end + +-------------------------------------------------------------------------------- +-- write, +-- @param {table} event, the event from broker +-- @return {boolean} +-------------------------------------------------------------------------------- +function write (event) + -- skip event if a mandatory parameter is missing + if queue.fail then + queue.sc_logger:error("Skipping event because a mandatory parameter is not set") + return false + end + + -- initiate event object + queue.sc_event = sc_event.new(event, queue.sc_params.params, queue.sc_common, queue.sc_logger, queue.sc_broker) + + if queue.sc_event:is_valid_category() then + if queue.sc_event:is_valid_element() then + --[[ + since broker version 3, event is a userdata and not a table. + This stream connector doesn't format the event + therefore it will send an event with only informatin from the cache + and some information that are spefically handled in the sc_event lib. + To have access to all the data, we "dump" everything from the userdata into the event table + ]]-- + for index, value in pairs(event) do + queue.sc_event.event[index] = value + end + + -- format event if it is validated + if queue.sc_event:is_valid_event() then + queue:format_accepted_event() + end + --- log why the event has been dropped + else + queue.sc_logger:debug("dropping event because element is not valid. Event element is: " + .. tostring(queue.sc_params.params.reverse_element_mapping[queue.sc_event.event.category][queue.sc_event.event.element])) + end + else + queue.sc_logger:debug("dropping event because category is not valid. Event category is: " + .. tostring(queue.sc_params.params.reverse_category_mapping[queue.sc_event.event.category])) + end + + return flush() +end + +-- flush method is called by broker every now and then (more often when broker has nothing else to do) +function flush() + local queues_size = queue.sc_flush:get_queues_size() + + -- nothing to flush + if queues_size == 0 then + return true + end + + -- flush all queues because last global flush is too old + if queue.sc_flush.last_global_flush < os.time() - queue.sc_params.params.max_all_queues_age then + if not queue.sc_flush:flush_all_queues(queue.build_payload_method[1], queue.send_data_method[1]) then + return false + end + + return true + end + + -- flush queues because too many events are stored in them + if queues_size > queue.sc_params.params.max_buffer_size then + if not queue.sc_flush:flush_all_queues(queue.build_payload_method[1], queue.send_data_method[1]) then + return false + end + + return true + end + + -- there are events in the queue but they were not ready to be send + return false +end diff --git a/modules/centreon-stream-connectors-lib/sc_webserver.lua b/modules/centreon-stream-connectors-lib/sc_webserver.lua index 530a6910..1a671030 100644 --- a/modules/centreon-stream-connectors-lib/sc_webserver.lua +++ b/modules/centreon-stream-connectors-lib/sc_webserver.lua @@ -61,8 +61,8 @@ function ScWebserver:start() local try = 1 local err - -- address may still be bound from previous execution. We do 10 retry before giving up - while not ok and try < 10 do + -- address may still be bound from previous execution. We do 60 retry before giving up + while not ok and try < 60 do err = "sc_webserver: failed to bind to " .. self.params.webserver_listen_address .. ":" .. tostring(self.params.webserver_port) .. " - " .. tostring(bind_err) self.sc_logger:error("[sc_webserver:start]: " .. err) self.sc_common:sleep(1) From 9a07bcb11ac6b7936bb04b00831c4741f9e2100e Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Tue, 9 Jun 2026 11:14:30 +0200 Subject: [PATCH 08/10] add getopt and json libs --- .../sc_getopt.lua | 70 + .../sc_json.lua | 1559 +++++++++++++++++ 2 files changed, 1629 insertions(+) create mode 100644 modules/centreon-stream-connectors-lib/sc_getopt.lua create mode 100644 modules/centreon-stream-connectors-lib/sc_json.lua diff --git a/modules/centreon-stream-connectors-lib/sc_getopt.lua b/modules/centreon-stream-connectors-lib/sc_getopt.lua new file mode 100644 index 00000000..254372dd --- /dev/null +++ b/modules/centreon-stream-connectors-lib/sc_getopt.lua @@ -0,0 +1,70 @@ +--- fork of https://github.com/skeeto/getopt-lua/blob/master/getopt.lua credits to @skeeto + + +--- getopt(argv, optstring [, nonoptions]) +-- +-- Returns a closure suitable for "for ... in" loops. On each call the +-- closure returns the next (option, optarg). For unknown options, it +-- returns ('?', option). When a required optarg is missing, it returns +-- (':', option). It's reasonable to continue parsing after errors. +-- Returns nil when done. +-- +-- The optstring follows the same format as POSIX getopt(3). However, +-- this function will never print output on its own. +-- +-- Non-option arguments are accumulated, in order, in the optional +-- "nonoptions" table. If a "--" argument is encountered, appends the +-- remaining arguments to the nonoptions table and returns nil. +-- +-- The input argv table is left unmodified. +local function getopt(argv, optstring, nonoptions) + local optind = 1 + local optpos = 2 + nonoptions = nonoptions or {} + return function() + while true do + local arg = argv[optind] + if arg == nil then + return nil + elseif arg == '--' then + for i = optind + 1, #argv do + table.insert(nonoptions, argv[i]) + end + return nil + elseif arg:sub(1, 1) == '-' then + local opt = arg:sub(optpos, optpos) + local start, stop = optstring:find(opt .. ':?') + if not start then + optind = optind + 1 + optpos = 2 + return '?', opt + elseif stop > start and #arg > optpos then + local optarg = arg:sub(optpos + 1) + optind = optind + 1 + optpos = 2 + return opt, optarg + elseif stop > start then + local optarg = argv[optind + 1] + optind = optind + 2 + optpos = 2 + if optarg == nil then + return ':', opt + end + return opt, optarg + else + optpos = optpos + 1 + if optpos > #arg then + optind = optind + 1 + optpos = 2 + end + return opt, nil + end + else + optind = optind + 1 + table.insert(nonoptions, arg) + end + end + end +end + +return getopt diff --git a/modules/centreon-stream-connectors-lib/sc_json.lua b/modules/centreon-stream-connectors-lib/sc_json.lua new file mode 100644 index 00000000..e853c05d --- /dev/null +++ b/modules/centreon-stream-connectors-lib/sc_json.lua @@ -0,0 +1,1559 @@ +-- -*- coding: utf-8 -*- +-- +-- Simple JSON encoding and decoding in pure Lua. +-- +-- Copyright 2010-2016 Jeffrey Friedl +-- http://regex.info/blog/ +-- Latest version: http://regex.info/blog/lua/json +-- +-- This code is released under a Creative Commons CC-BY "Attribution" License: +-- http://creativecommons.org/licenses/by/3.0/deed.en_US +-- +-- It can be used for any purpose so long as: +-- 1) the copyright notice above is maintained +-- 2) the web-page links above are maintained +-- 3) the 'AUTHOR_NOTE' string below is maintained +-- +local VERSION = '20161109.21' -- version history at end of file +local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20161109.21 ]-" + +-- +-- The 'AUTHOR_NOTE' variable exists so that information about the source +-- of the package is maintained even in compiled versions. It's also +-- included in OBJDEF below mostly to quiet warnings about unused variables. +-- +local OBJDEF = { + VERSION = VERSION, + AUTHOR_NOTE = AUTHOR_NOTE, +} + + +-- +-- Simple JSON encoding and decoding in pure Lua. +-- JSON definition: http://www.json.org/ +-- +-- +-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local lua_value = JSON:decode(raw_json_text) +-- +-- local raw_json_text = JSON:encode(lua_table_or_value) +-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability +-- +-- +-- +-- DECODING (from a JSON string to a Lua table) +-- +-- +-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local lua_value = JSON:decode(raw_json_text) +-- +-- If the JSON text is for an object or an array, e.g. +-- { "what": "books", "count": 3 } +-- or +-- [ "Larry", "Curly", "Moe" ] +-- +-- the result is a Lua table, e.g. +-- { what = "books", count = 3 } +-- or +-- { "Larry", "Curly", "Moe" } +-- +-- +-- The encode and decode routines accept an optional second argument, +-- "etc", which is not used during encoding or decoding, but upon error +-- is passed along to error handlers. It can be of any type (including nil). +-- +-- +-- +-- ERROR HANDLING +-- +-- With most errors during decoding, this code calls +-- +-- JSON:onDecodeError(message, text, location, etc) +-- +-- with a message about the error, and if known, the JSON text being +-- parsed and the byte count where the problem was discovered. You can +-- replace the default JSON:onDecodeError() with your own function. +-- +-- The default onDecodeError() merely augments the message with data +-- about the text and the location if known (and if a second 'etc' +-- argument had been provided to decode(), its value is tacked onto the +-- message as well), and then calls JSON.assert(), which itself defaults +-- to Lua's built-in assert(), and can also be overridden. +-- +-- For example, in an Adobe Lightroom plugin, you might use something like +-- +-- function JSON:onDecodeError(message, text, location, etc) +-- LrErrors.throwUserError("Internal Error: invalid JSON data") +-- end +-- +-- or even just +-- +-- function JSON.assert(message) +-- LrErrors.throwUserError("Internal Error: " .. message) +-- end +-- +-- If JSON:decode() is passed a nil, this is called instead: +-- +-- JSON:onDecodeOfNilError(message, nil, nil, etc) +-- +-- and if JSON:decode() is passed HTML instead of JSON, this is called: +-- +-- JSON:onDecodeOfHTMLError(message, text, nil, etc) +-- +-- The use of the fourth 'etc' argument allows stronger coordination +-- between decoding and error reporting, especially when you provide your +-- own error-handling routines. Continuing with the the Adobe Lightroom +-- plugin example: +-- +-- function JSON:onDecodeError(message, text, location, etc) +-- local note = "Internal Error: invalid JSON data" +-- if type(etc) = 'table' and etc.photo then +-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') +-- end +-- LrErrors.throwUserError(note) +-- end +-- +-- : +-- : +-- +-- for i, photo in ipairs(photosToProcess) do +-- : +-- : +-- local data = JSON:decode(someJsonText, { photo = photo }) +-- : +-- : +-- end +-- +-- +-- +-- If the JSON text passed to decode() has trailing garbage (e.g. as with the JSON "[123]xyzzy"), +-- the method +-- +-- JSON:onTrailingGarbage(json_text, location, parsed_value, etc) +-- +-- is invoked, where: +-- +-- json_text is the original JSON text being parsed, +-- location is the count of bytes into json_text where the garbage starts (6 in the example), +-- parsed_value is the Lua result of what was successfully parsed ({123} in the example), +-- etc is as above. +-- +-- If JSON:onTrailingGarbage() does not abort, it should return the value decode() should return, +-- or nil + an error message. +-- +-- local new_value, error_message = JSON:onTrailingGarbage() +-- +-- The default handler just invokes JSON:onDecodeError("trailing garbage"...), but you can have +-- this package ignore trailing garbage via +-- +-- function JSON:onTrailingGarbage(json_text, location, parsed_value, etc) +-- return parsed_value +-- end +-- +-- +-- DECODING AND STRICT TYPES +-- +-- Because both JSON objects and JSON arrays are converted to Lua tables, +-- it's not normally possible to tell which original JSON type a +-- particular Lua table was derived from, or guarantee decode-encode +-- round-trip equivalency. +-- +-- However, if you enable strictTypes, e.g. +-- +-- JSON = assert(loadfile "JSON.lua")() --load the routines +-- JSON.strictTypes = true +-- +-- then the Lua table resulting from the decoding of a JSON object or +-- JSON array is marked via Lua metatable, so that when re-encoded with +-- JSON:encode() it ends up as the appropriate JSON type. +-- +-- (This is not the default because other routines may not work well with +-- tables that have a metatable set, for example, Lightroom API calls.) +-- +-- +-- ENCODING (from a lua table to a JSON string) +-- +-- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local raw_json_text = JSON:encode(lua_table_or_value) +-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability +-- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false }) +-- +-- On error during encoding, this code calls: +-- +-- JSON:onEncodeError(message, etc) +-- +-- which you can override in your local JSON object. +-- +-- The 'etc' in the error call is the second argument to encode() +-- and encode_pretty(), or nil if it wasn't provided. +-- +-- +-- ENCODING OPTIONS +-- +-- An optional third argument, a table of options, can be provided to encode(). +-- +-- encode_options = { +-- -- options for making "pretty" human-readable JSON (see "PRETTY-PRINTING" below) +-- pretty = true, +-- indent = " ", +-- align_keys = false, +-- array_newline = false, +-- +-- -- other output-related options +-- null = "\0", -- see "ENCODING JSON NULL VALUES" below +-- stringsAreUtf8 = false, -- see "HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA" below +-- } +-- +-- json_string = JSON:encode(mytable, etc, encode_options) +-- +-- +-- +-- For reference, the defaults are: +-- +-- pretty = false +-- null = nil, +-- stringsAreUtf8 = false, +-- array_newline = false, +-- +-- +-- +-- PRETTY-PRINTING +-- +-- Enabling the 'pretty' encode option helps generate human-readable JSON. +-- +-- pretty = JSON:encode(val, etc, { +-- pretty = true, +-- indent = " ", +-- align_keys = false, +-- }) +-- +-- encode_pretty() is also provided: it's identical to encode() except +-- that encode_pretty() provides a default options table if none given in the call: +-- +-- { pretty = true, align_keys = false, indent = " " } +-- +-- For example, if +-- +-- JSON:encode(data) +-- +-- produces: +-- +-- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} +-- +-- then +-- +-- JSON:encode_pretty(data) +-- +-- produces: +-- +-- { +-- "city": "Kyoto", +-- "climate": { +-- "avg_temp": 16, +-- "humidity": "high", +-- "snowfall": "minimal" +-- }, +-- "country": "Japan", +-- "wards": 11 +-- } +-- +-- The following three lines return identical results: +-- JSON:encode_pretty(data) +-- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " }) +-- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " }) +-- +-- An example of setting your own indent string: +-- +-- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " }) +-- +-- produces: +-- +-- { +-- | "city": "Kyoto", +-- | "climate": { +-- | | "avg_temp": 16, +-- | | "humidity": "high", +-- | | "snowfall": "minimal" +-- | }, +-- | "country": "Japan", +-- | "wards": 11 +-- } +-- +-- An example of setting align_keys to true: +-- +-- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true }) +-- +-- produces: +-- +-- { +-- "city": "Kyoto", +-- "climate": { +-- "avg_temp": 16, +-- "humidity": "high", +-- "snowfall": "minimal" +-- }, +-- "country": "Japan", +-- "wards": 11 +-- } +-- +-- which I must admit is kinda ugly, sorry. This was the default for +-- encode_pretty() prior to version 20141223.14. +-- +-- +-- HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA +-- +-- If the 'stringsAreUtf8' encode option is set to true, consider Lua strings not as a sequence of bytes, +-- but as a sequence of UTF-8 characters. +-- +-- Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH +-- separators, if found in a string, are encoded with a JSON escape instead of being dumped as is. +-- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON +-- to also be valid Java. +-- +-- AMBIGUOUS SITUATIONS DURING THE ENCODING +-- +-- During the encode, if a Lua table being encoded contains both string +-- and numeric keys, it fits neither JSON's idea of an object, nor its +-- idea of an array. To get around this, when any string key exists (or +-- when non-positive numeric keys exist), numeric keys are converted to +-- strings. +-- +-- For example, +-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) +-- produces the JSON object +-- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} +-- +-- To prohibit this conversion and instead make it an error condition, set +-- JSON.noKeyConversion = true +-- +-- +-- ENCODING JSON NULL VALUES +-- +-- Lua tables completely omit keys whose value is nil, so without special handling there's +-- no way to get a field in a JSON object with a null value. For example +-- JSON:encode({ username = "admin", password = nil }) +-- produces +-- {"username":"admin"} +-- +-- In order to actually produce +-- {"username":"admin", "password":null} +-- one can include a string value for a "null" field in the options table passed to encode().... +-- any Lua table entry with that value becomes null in the JSON output: +-- JSON:encode({ username = "admin", password = "xyzzy" }, nil, { null = "xyzzy" }) +-- produces +-- {"username":"admin", "password":null} +-- +-- Just be sure to use a string that is otherwise unlikely to appear in your data. +-- The string "\0" (a string with one null byte) may well be appropriate for many applications. +-- +-- The "null" options also applies to Lua tables that become JSON arrays. +-- JSON:encode({ "one", "two", nil, nil }) +-- produces +-- ["one","two"] +-- while +-- NULL = "\0" +-- JSON:encode({ "one", "two", NULL, NULL}, nil, { null = NULL }) +-- produces +-- ["one","two",null,null] +-- +-- +-- +-- +-- HANDLING LARGE AND/OR PRECISE NUMBERS +-- +-- +-- Without special handling, numbers in JSON can lose precision in Lua. +-- For example: +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- produces +-- +-- small: number 12345 +-- big: number 1.2345678901235e+28 +-- precise: number 9876.6789012346 +-- +-- Precision is lost with both 'big' and 'precise'. +-- +-- This package offers ways to try to handle this better (for some definitions of "better")... +-- +-- The most precise method is by setting the global: +-- +-- JSON.decodeNumbersAsObjects = true +-- +-- When this is set, numeric JSON data is encoded into Lua in a form that preserves the exact +-- JSON numeric presentation when re-encoded back out to JSON, or accessed in Lua as a string. +-- +-- (This is done by encoding the numeric data with a Lua table/metatable that returns +-- the possibly-imprecise numeric form when accessed numerically, but the original precise +-- representation when accessed as a string. You can also explicitly access +-- via JSON:forceString() and JSON:forceNumber()) +-- +-- Consider the example above, with this option turned on: +-- +-- JSON.decodeNumbersAsObjects = true +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- This now produces: +-- +-- small: table 12345 +-- big: table 12345678901234567890123456789 +-- precise: table 9876.67890123456789012345 +-- +-- However, within Lua you can still use the values (e.g. T.precise in the example above) in numeric +-- contexts. In such cases you'll get the possibly-imprecise numeric version, but in string contexts +-- and when the data finds its way to this package's encode() function, the original full-precision +-- representation is used. +-- +-- Even without using the JSON.decodeNumbersAsObjects option, you can encode numbers +-- in your Lua table that retain high precision upon encoding to JSON, by using the JSON:asNumber() +-- function: +-- +-- T = { +-- imprecise = 123456789123456789.123456789123456789, +-- precise = JSON:asNumber("123456789123456789.123456789123456789") +-- } +-- +-- print(JSON:encode_pretty(T)) +-- +-- This produces: +-- +-- { +-- "precise": 123456789123456789.123456789123456789, +-- "imprecise": 1.2345678912346e+17 +-- } +-- +-- +-- +-- A different way to handle big/precise JSON numbers is to have decode() merely return +-- the exact string representation of the number instead of the number itself. +-- This approach might be useful when the numbers are merely some kind of opaque +-- object identifier and you want to work with them in Lua as strings anyway. +-- +-- This approach is enabled by setting +-- +-- JSON.decodeIntegerStringificationLength = 10 +-- +-- The value is the number of digits (of the integer part of the number) at which to stringify numbers. +-- +-- Consider our previous example with this option set to 10: +-- +-- JSON.decodeIntegerStringificationLength = 10 +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- This produces: +-- +-- small: number 12345 +-- big: string 12345678901234567890123456789 +-- precise: number 9876.6789012346 +-- +-- The long integer of the 'big' field is at least JSON.decodeIntegerStringificationLength digits +-- in length, so it's converted not to a Lua integer but to a Lua string. Using a value of 0 or 1 ensures +-- that all JSON numeric data becomes strings in Lua. +-- +-- Note that unlike +-- JSON.decodeNumbersAsObjects = true +-- this stringification is simple and unintelligent: the JSON number simply becomes a Lua string, and that's the end of it. +-- If the string is then converted back to JSON, it's still a string. After running the code above, adding +-- print(JSON:encode(T)) +-- produces +-- {"big":"12345678901234567890123456789","precise":9876.6789012346,"small":12345} +-- which is unlikely to be desired. +-- +-- There's a comparable option for the length of the decimal part of a number: +-- +-- JSON.decodeDecimalStringificationLength +-- +-- This can be used alone or in conjunction with +-- +-- JSON.decodeIntegerStringificationLength +-- +-- to trip stringification on precise numbers with at least JSON.decodeIntegerStringificationLength digits after +-- the decimal point. +-- +-- This example: +-- +-- JSON.decodeIntegerStringificationLength = 10 +-- JSON.decodeDecimalStringificationLength = 5 +-- +-- T = JSON:decode('{ "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345 }') +-- +-- print("small: ", type(T.small), T.small) +-- print("big: ", type(T.big), T.big) +-- print("precise: ", type(T.precise), T.precise) +-- +-- produces: +-- +-- small: number 12345 +-- big: string 12345678901234567890123456789 +-- precise: string 9876.67890123456789012345 +-- +-- +-- +-- +-- +-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT +-- +-- assert +-- onDecodeError +-- onDecodeOfNilError +-- onDecodeOfHTMLError +-- onTrailingGarbage +-- onEncodeError +-- +-- If you want to create a separate Lua JSON object with its own error handlers, +-- you can reload JSON.lua or use the :new() method. +-- +--------------------------------------------------------------------------- + +local default_pretty_indent = " " +local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } + +local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray +local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject + +function OBJDEF:newArray(tbl) + return setmetatable(tbl or {}, isArray) +end + +function OBJDEF:newObject(tbl) + return setmetatable(tbl or {}, isObject) +end + + + + +local function getnum(op) + return type(op) == 'number' and op or op.N +end + +local isNumber = { + __tostring = function(T) return T.S end, + __unm = function(op) return getnum(op) end, + + __concat = function(op1, op2) return tostring(op1) .. tostring(op2) end, + __add = function(op1, op2) return getnum(op1) + getnum(op2) end, + __sub = function(op1, op2) return getnum(op1) - getnum(op2) end, + __mul = function(op1, op2) return getnum(op1) * getnum(op2) end, + __div = function(op1, op2) return getnum(op1) / getnum(op2) end, + __mod = function(op1, op2) return getnum(op1) % getnum(op2) end, + __pow = function(op1, op2) return getnum(op1) ^ getnum(op2) end, + __lt = function(op1, op2) return getnum(op1) < getnum(op2) end, + __eq = function(op1, op2) return getnum(op1) == getnum(op2) end, + __le = function(op1, op2) return getnum(op1) <= getnum(op2) end, +} +isNumber.__index = isNumber + +function OBJDEF:asNumber(item) + + if getmetatable(item) == isNumber then + -- it's already a JSON number object. + return item + elseif type(item) == 'table' and type(item.S) == 'string' and type(item.N) == 'number' then + -- it's a number-object table that lost its metatable, so give it one + return setmetatable(item, isNumber) + else + -- the normal situation... given a number or a string representation of a number.... + local holder = { + S = tostring(item), -- S is the representation of the number as a string, which remains precise + N = tonumber(item), -- N is the number as a Lua number. + } + return setmetatable(holder, isNumber) + end +end + +-- +-- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, +-- return the string version. This shouldn't be needed often because the 'isNumber' object should autoconvert +-- to a string in most cases, but it's here to allow it to be forced when needed. +-- +function OBJDEF:forceString(item) + if type(item) == 'table' and type(item.S) == 'string' then + return item.S + else + return tostring(item) + end +end + +-- +-- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above, +-- return the numeric version. +-- +function OBJDEF:forceNumber(item) + if type(item) == 'table' and type(item.N) == 'number' then + return item.N + else + return tonumber(item) + end +end + + +local function unicode_codepoint_as_utf8(codepoint) + -- + -- codepoint is a number + -- + if codepoint <= 127 then + return string.char(codepoint) + + elseif codepoint <= 2047 then + -- + -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 + -- + local highpart = math.floor(codepoint / 0x40) + local lowpart = codepoint - (0x40 * highpart) + return string.char(0xC0 + highpart, + 0x80 + lowpart) + + elseif codepoint <= 65535 then + -- + -- 1110yyyy 10yyyyxx 10xxxxxx + -- + local highpart = math.floor(codepoint / 0x1000) + local remainder = codepoint - 0x1000 * highpart + local midpart = math.floor(remainder / 0x40) + local lowpart = remainder - 0x40 * midpart + + highpart = 0xE0 + highpart + midpart = 0x80 + midpart + lowpart = 0x80 + lowpart + + -- + -- Check for an invalid character (thanks Andy R. at Adobe). + -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 + -- + if ( highpart == 0xE0 and midpart < 0xA0 ) or + ( highpart == 0xED and midpart > 0x9F ) or + ( highpart == 0xF0 and midpart < 0x90 ) or + ( highpart == 0xF4 and midpart > 0x8F ) + then + return "?" + else + return string.char(highpart, + midpart, + lowpart) + end + + else + -- + -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx + -- + local highpart = math.floor(codepoint / 0x40000) + local remainder = codepoint - 0x40000 * highpart + local midA = math.floor(remainder / 0x1000) + remainder = remainder - 0x1000 * midA + local midB = math.floor(remainder / 0x40) + local lowpart = remainder - 0x40 * midB + + return string.char(0xF0 + highpart, + 0x80 + midA, + 0x80 + midB, + 0x80 + lowpart) + end +end + +function OBJDEF:onDecodeError(message, text, location, etc) + if text then + if location then + message = string.format("%s at byte %d of: %s", message, location, text) + else + message = string.format("%s: %s", message, text) + end + end + + if etc ~= nil then + message = message .. " (" .. OBJDEF:encode(etc) .. ")" + end + + if self.assert then + self.assert(false, message) + else + assert(false, message) + end +end + +function OBJDEF:onTrailingGarbage(json_text, location, parsed_value, etc) + return self:onDecodeError("trailing garbage", json_text, location, etc) +end + +OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError +OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError + +function OBJDEF:onEncodeError(message, etc) + if etc ~= nil then + message = message .. " (" .. OBJDEF:encode(etc) .. ")" + end + + if self.assert then + self.assert(false, message) + else + assert(false, message) + end +end + +local function grok_number(self, text, start, options) + -- + -- Grab the integer part + -- + local integer_part = text:match('^-?[1-9]%d*', start) + or text:match("^-?0", start) + + if not integer_part then + self:onDecodeError("expected number", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + local i = start + integer_part:len() + + -- + -- Grab an optional decimal part + -- + local decimal_part = text:match('^%.%d+', i) or "" + + i = i + decimal_part:len() + + -- + -- Grab an optional exponential part + -- + local exponent_part = text:match('^[eE][-+]?%d+', i) or "" + + i = i + exponent_part:len() + + local full_number_text = integer_part .. decimal_part .. exponent_part + + if options.decodeNumbersAsObjects then + return OBJDEF:asNumber(full_number_text), i + end + + -- + -- If we're told to stringify under certain conditions, so do. + -- We punt a bit when there's an exponent by just stringifying no matter what. + -- I suppose we should really look to see whether the exponent is actually big enough one + -- way or the other to trip stringification, but I'll be lazy about it until someone asks. + -- + if (options.decodeIntegerStringificationLength + and + (integer_part:len() >= options.decodeIntegerStringificationLength or exponent_part:len() > 0)) + + or + + (options.decodeDecimalStringificationLength + and + (decimal_part:len() >= options.decodeDecimalStringificationLength or exponent_part:len() > 0)) + then + return full_number_text, i -- this returns the exact string representation seen in the original JSON + end + + + + local as_number = tonumber(full_number_text) + + if not as_number then + self:onDecodeError("bad number", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + return as_number, i +end + + +local function grok_string(self, text, start, options) + + if text:sub(start,start) ~= '"' then + self:onDecodeError("expected string's opening quote", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + local i = start + 1 -- +1 to bypass the initial quote + local text_len = text:len() + local VALUE = "" + while i <= text_len do + local c = text:sub(i,i) + if c == '"' then + return VALUE, i + 1 + end + if c ~= '\\' then + VALUE = VALUE .. c + i = i + 1 + elseif text:match('^\\b', i) then + VALUE = VALUE .. "\b" + i = i + 2 + elseif text:match('^\\f', i) then + VALUE = VALUE .. "\f" + i = i + 2 + elseif text:match('^\\n', i) then + VALUE = VALUE .. "\n" + i = i + 2 + elseif text:match('^\\r', i) then + VALUE = VALUE .. "\r" + i = i + 2 + elseif text:match('^\\t', i) then + VALUE = VALUE .. "\t" + i = i + 2 + else + local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if hex then + i = i + 6 -- bypass what we just read + + -- We have a Unicode codepoint. It could be standalone, or if in the proper range and + -- followed by another in a specific range, it'll be a two-code surrogate pair. + local codepoint = tonumber(hex, 16) + if codepoint >= 0xD800 and codepoint <= 0xDBFF then + -- it's a hi surrogate... see whether we have a following low + local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if lo_surrogate then + i = i + 6 -- bypass the low surrogate we just read + codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) + else + -- not a proper low, so we'll just leave the first codepoint as is and spit it out. + end + end + VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) + + else + + -- just pass through what's escaped + VALUE = VALUE .. text:match('^\\(.)', i) + i = i + 2 + end + end + end + + self:onDecodeError("unclosed string", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible +end + +local function skip_whitespace(text, start) + + local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 + if match_end then + return match_end + 1 + else + return start + end +end + +local grok_one -- assigned later + +local function grok_object(self, text, start, options) + + if text:sub(start,start) ~= '{' then + self:onDecodeError("expected '{'", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' + + local VALUE = self.strictTypes and self:newObject { } or { } + + if text:sub(i,i) == '}' then + return VALUE, i + 1 + end + local text_len = text:len() + while i <= text_len do + local key, new_i = grok_string(self, text, i, options) + + i = skip_whitespace(text, new_i) + + if text:sub(i, i) ~= ':' then + self:onDecodeError("expected colon", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible + end + + i = skip_whitespace(text, i + 1) + + local new_val, new_i = grok_one(self, text, i, options) + + VALUE[key] = new_val + + -- + -- Expect now either '}' to end things, or a ',' to allow us to continue. + -- + i = skip_whitespace(text, new_i) + + local c = text:sub(i,i) + + if c == '}' then + return VALUE, i + 1 + end + + if text:sub(i, i) ~= ',' then + self:onDecodeError("expected comma or '}'", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible + end + + i = skip_whitespace(text, i + 1) + end + + self:onDecodeError("unclosed '{'", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible +end + +local function grok_array(self, text, start, options) + if text:sub(start,start) ~= '[' then + self:onDecodeError("expected '['", text, start, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' + local VALUE = self.strictTypes and self:newArray { } or { } + if text:sub(i,i) == ']' then + return VALUE, i + 1 + end + + local VALUE_INDEX = 1 + + local text_len = text:len() + while i <= text_len do + local val, new_i = grok_one(self, text, i, options) + + -- can't table.insert(VALUE, val) here because it's a no-op if val is nil + VALUE[VALUE_INDEX] = val + VALUE_INDEX = VALUE_INDEX + 1 + + i = skip_whitespace(text, new_i) + + -- + -- Expect now either ']' to end things, or a ',' to allow us to continue. + -- + local c = text:sub(i,i) + if c == ']' then + return VALUE, i + 1 + end + if text:sub(i, i) ~= ',' then + self:onDecodeError("expected comma or ']'", text, i, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible + end + i = skip_whitespace(text, i + 1) + end + self:onDecodeError("unclosed '['", text, start, options.etc) + return nil, i -- in case the error method doesn't abort, return something sensible +end + + +grok_one = function(self, text, start, options) + -- Skip any whitespace + start = skip_whitespace(text, start) + + if start > text:len() then + self:onDecodeError("unexpected end of string", text, nil, options.etc) + return nil, start -- in case the error method doesn't abort, return something sensible + end + + if text:find('^"', start) then + return grok_string(self, text, start, options.etc) + + elseif text:find('^[-0123456789 ]', start) then + return grok_number(self, text, start, options) + + elseif text:find('^%{', start) then + return grok_object(self, text, start, options) + + elseif text:find('^%[', start) then + return grok_array(self, text, start, options) + + elseif text:find('^true', start) then + return true, start + 4 + + elseif text:find('^false', start) then + return false, start + 5 + + elseif text:find('^null', start) then + return nil, start + 4 + + else + self:onDecodeError("can't parse JSON", text, start, options.etc) + return nil, 1 -- in case the error method doesn't abort, return something sensible + end +end + +function OBJDEF:decode(text, etc, options) + -- + -- If the user didn't pass in a table of decode options, make an empty one. + -- + if type(options) ~= 'table' then + options = {} + end + + -- + -- If they passed in an 'etc' argument, stuff it into the options. + -- (If not, any 'etc' field in the options they passed in remains to be used) + -- + if etc ~= nil then + options.etc = etc + end + + + if type(self) ~= 'table' or self.__index ~= OBJDEF then + local error_message = "JSON:decode must be called in method format" + OBJDEF:onDecodeError(error_message, nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + end + + if text == nil then + local error_message = "nil passed to JSON:decode()" + self:onDecodeOfNilError(error_message, nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + + elseif type(text) ~= 'string' then + local error_message = "expected string argument to JSON:decode()" + self:onDecodeError(string.format("%s, got %s", error_message, type(text)), nil, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + end + + if text:match('^%s*$') then + -- an empty string is nothing, but not an error + return nil + end + + if text:match('^%s*<') then + -- Can't be JSON... we'll assume it's HTML + local error_message = "HTML passed to JSON:decode()" + self:onDecodeOfHTMLError(error_message, text, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + end + + -- + -- Ensure that it's not UTF-32 or UTF-16. + -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), + -- but this package can't handle them. + -- + if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then + local error_message = "JSON package groks only UTF-8, sorry" + self:onDecodeError(error_message, text, nil, options.etc) + return nil, error_message -- in case the error method doesn't abort, return something sensible + end + + -- + -- apply global options + -- + if options.decodeNumbersAsObjects == nil then + options.decodeNumbersAsObjects = self.decodeNumbersAsObjects + end + if options.decodeIntegerStringificationLength == nil then + options.decodeIntegerStringificationLength = self.decodeIntegerStringificationLength + end + if options.decodeDecimalStringificationLength == nil then + options.decodeDecimalStringificationLength = self.decodeDecimalStringificationLength + end + + -- + -- Finally, go parse it + -- + local success, value, next_i = pcall(grok_one, self, text, 1, options) + + if success then + + local error_message = nil + if next_i ~= #text + 1 then + -- something's left over after we parsed the first thing.... whitespace is allowed. + next_i = skip_whitespace(text, next_i) + + -- if we have something left over now, it's trailing garbage + if next_i ~= #text + 1 then + value, error_message = self:onTrailingGarbage(text, next_i, value, options.etc) + end + end + return value, error_message + + else + + -- If JSON:onDecodeError() didn't abort out of the pcall, we'll have received + -- the error message here as "value", so pass it along as an assert. + local error_message = value + if self.assert then + self.assert(false, error_message) + else + assert(false, error_message) + end + -- ...and if we're still here (because the assert didn't throw an error), + -- return a nil and throw the error message on as a second arg + return nil, error_message + + end +end + +local function backslash_replacement_function(c) + if c == "\n" then + return "\\n" + elseif c == "\r" then + return "\\r" + elseif c == "\t" then + return "\\t" + elseif c == "\b" then + return "\\b" + elseif c == "\f" then + return "\\f" + elseif c == '"' then + return '\\"' + elseif c == '\\' then + return '\\\\' + else + return string.format("\\u%04x", c:byte()) + end +end + +local chars_to_be_escaped_in_JSON_string + = '[' + .. '"' -- class sub-pattern to match a double quote + .. '%\\' -- class sub-pattern to match a backslash + .. '%z' -- class sub-pattern to match a null + .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters + .. ']' + + +local LINE_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2028) +local PARAGRAPH_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2029) +local function json_string_literal(value, options) + local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) + if options.stringsAreUtf8 then + -- + -- This feels really ugly to just look into a string for the sequence of bytes that we know to be a particular utf8 character, + -- but utf8 was designed purposefully to make this kind of thing possible. Still, feels dirty. + -- I'd rather decode the byte stream into a character stream, but it's not technically needed so + -- not technically worth it. + -- + newval = newval:gsub(LINE_SEPARATOR_as_utf8, '\\u2028'):gsub(PARAGRAPH_SEPARATOR_as_utf8,'\\u2029') + end + return '"' .. newval .. '"' +end + +local function object_or_array(self, T, etc) + -- + -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON + -- object. If there are only numbers, it's a JSON array. + -- + -- If we'll be converting to a JSON object, we'll want to sort the keys so that the + -- end result is deterministic. + -- + local string_keys = { } + local number_keys = { } + local number_keys_must_be_strings = false + local maximum_number_key + + for key in pairs(T) do + if type(key) == 'string' then + table.insert(string_keys, key) + elseif type(key) == 'number' then + table.insert(number_keys, key) + if key <= 0 or key >= math.huge then + number_keys_must_be_strings = true + elseif not maximum_number_key or key > maximum_number_key then + maximum_number_key = key + end + else + self:onEncodeError("can't encode table with a key of type " .. type(key), etc) + end + end + + if #string_keys == 0 and not number_keys_must_be_strings then + -- + -- An empty table, or a numeric-only array + -- + if #number_keys > 0 then + return nil, maximum_number_key -- an array + elseif tostring(T) == "JSON array" then + return nil + elseif tostring(T) == "JSON object" then + return { } + else + -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects + return nil + end + end + + table.sort(string_keys) + + local map + if #number_keys > 0 then + -- + -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array + -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object. + -- + + if self.noKeyConversion then + self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) + end + + -- + -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings + -- + map = { } + for key, val in pairs(T) do + map[key] = val + end + + table.sort(number_keys) + + -- + -- Throw numeric keys in there as strings + -- + for _, number_key in ipairs(number_keys) do + local string_key = tostring(number_key) + if map[string_key] == nil then + table.insert(string_keys , string_key) + map[string_key] = T[number_key] + else + self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc) + end + end + end + + return string_keys, nil, map +end + +-- +-- Encode +-- +-- 'options' is nil, or a table with possible keys: +-- +-- pretty -- If true, return a pretty-printed version. +-- +-- indent -- A string (usually of spaces) used to indent each nested level. +-- +-- align_keys -- If true, align all the keys when formatting a table. +-- +-- null -- If this exists with a string value, table elements with this value are output as JSON null. +-- +-- stringsAreUtf8 -- If true, consider Lua strings not as a sequence of bytes, but as a sequence of UTF-8 characters. +-- (Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH +-- separators, if found in a string, are encoded with a JSON escape instead of as raw UTF-8. +-- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON +-- to also be valid Java.) +-- +-- +local encode_value -- must predeclare because it calls itself +function encode_value(self, value, parents, etc, options, indent, for_key) + + -- + -- keys in a JSON object can never be null, so we don't even consider options.null when converting a key value + -- + if value == nil or (not for_key and options and options.null and value == options.null) then + return 'null' + + elseif type(value) == 'string' then + return json_string_literal(value, options) + + elseif type(value) == 'number' then + if value ~= value then + -- + -- NaN (Not a Number). + -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. + -- + return "null" + elseif value >= math.huge then + -- + -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should + -- really be a package option. Note: at least with some implementations, positive infinity + -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. + -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" + -- case first. + -- + return "1e+9999" + elseif value <= -math.huge then + -- + -- Negative infinity. + -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. + -- + return "-1e+9999" + else + return tostring(value) + end + + elseif type(value) == 'boolean' then + return tostring(value) + + elseif type(value) ~= 'table' then + self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + + elseif getmetatable(value) == isNumber then + return tostring(value) + else + -- + -- A table to be converted to either a JSON object or array. + -- + local T = value + + if type(options) ~= 'table' then + options = {} + end + if type(indent) ~= 'string' then + indent = "" + end + + if parents[T] then + self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) + else + parents[T] = true + end + + local result_value + + local object_keys, maximum_number_key, map = object_or_array(self, T, etc) + if maximum_number_key then + -- + -- An array... + -- + local ITEMS = { } + local key_indent = indent .. tostring(options.indent or "") + for i = 1, maximum_number_key do + if not options.array_newline then + table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) + else + table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, key_indent)) + end + end + + if options.pretty then + if not options.array_newline then + result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" + else + result_value = "[\n" .. key_indent .. table.concat(ITEMS, ",\n" .. key_indent) .. "\n" .. indent .. "]" + end + else + result_value = "[" .. table.concat(ITEMS, ",") .. "]" + end + + elseif object_keys then + -- + -- An object + -- + local TT = map or T + + if options.pretty then + + local KEYS = { } + local max_key_length = 0 + for _, key in ipairs(object_keys) do + local encoded = encode_value(self, tostring(key), parents, etc, options, indent, true) + if options.align_keys then + max_key_length = math.max(max_key_length, #encoded) + end + table.insert(KEYS, encoded) + end + local key_indent = indent .. tostring(options.indent or "") + local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "") + local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" + + local COMBINED_PARTS = { } + for i, key in ipairs(object_keys) do + local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) + table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) + end + result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" + + else + + local PARTS = { } + for _, key in ipairs(object_keys) do + local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) + local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent, true) + table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) + end + result_value = "{" .. table.concat(PARTS, ",") .. "}" + + end + else + -- + -- An empty array/object... we'll treat it as an array, though it should really be an option + -- + result_value = "[]" + end + + parents[T] = false + return result_value + end +end + +local function top_level_encode(self, value, etc, options) + local val = encode_value(self, value, {}, etc, options) + if val == nil then + --PRIVATE("may need to revert to the previous public verison if I can't figure out what the guy wanted") + return val + else + return val + end +end + +function OBJDEF:encode(value, etc, options) + if type(self) ~= 'table' or self.__index ~= OBJDEF then + OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) + end + + -- + -- If the user didn't pass in a table of decode options, make an empty one. + -- + if type(options) ~= 'table' then + options = {} + end + + return top_level_encode(self, value, etc, options) +end + +function OBJDEF:encode_pretty(value, etc, options) + if type(self) ~= 'table' or self.__index ~= OBJDEF then + OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) + end + + -- + -- If the user didn't pass in a table of decode options, use the default pretty ones + -- + if type(options) ~= 'table' then + options = default_pretty_options + end + + return top_level_encode(self, value, etc, options) +end + +function OBJDEF.__tostring() + return "JSON encode/decode package" +end + +OBJDEF.__index = OBJDEF + +function OBJDEF:new(args) + local new = { } + + if args then + for key, val in pairs(args) do + new[key] = val + end + end + + return setmetatable(new, OBJDEF) +end + +return OBJDEF:new() + +-- +-- Version history: +-- +-- 20161109.21 Oops, had a small boo-boo in the previous update. +-- +-- 20161103.20 Used to silently ignore trailing garbage when decoding. Now fails via JSON:onTrailingGarbage() +-- http://seriot.ch/parsing_json.php +-- +-- Built-in error message about "expected comma or ']'" had mistakenly referred to '[' +-- +-- Updated the built-in error reporting to refer to bytes rather than characters. +-- +-- The decode() method no longer assumes that error handlers abort. +-- +-- Made the VERSION string a string instead of a number +-- + +-- 20160916.19 Fixed the isNumber.__index assignment (thanks to Jack Taylor) +-- +-- 20160730.18 Added JSON:forceString() and JSON:forceNumber() +-- +-- 20160728.17 Added concatenation to the metatable for JSON:asNumber() +-- +-- 20160709.16 Could crash if not passed an options table (thanks jarno heikkinen ). +-- +-- Made JSON:asNumber() a bit more resilient to being passed the results of itself. +-- +-- 20160526.15 Added the ability to easily encode null values in JSON, via the new "null" encoding option. +-- (Thanks to Adam B for bringing up the issue.) +-- +-- Added some support for very large numbers and precise floats via +-- JSON.decodeNumbersAsObjects +-- JSON.decodeIntegerStringificationLength +-- JSON.decodeDecimalStringificationLength +-- +-- Added the "stringsAreUtf8" encoding option. (Hat tip to http://lua-users.org/wiki/JsonModules ) +-- +-- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really +-- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines +-- more flexible, and changed the default encode_pretty() to be more generally useful. +-- +-- Added a third 'options' argument to the encode() and encode_pretty() routines, to control +-- how the encoding takes place. +-- +-- Updated docs to add assert() call to the loadfile() line, just as good practice so that +-- if there is a problem loading JSON.lua, the appropriate error message will percolate up. +-- +-- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string, +-- so that the source of the package, and its version number, are visible in compiled copies. +-- +-- 20140911.12 Minor lua cleanup. +-- Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'. +-- (Thanks to SmugMug's David Parry for these.) +-- +-- 20140418.11 JSON nulls embedded within an array were being ignored, such that +-- ["1",null,null,null,null,null,"seven"], +-- would return +-- {1,"seven"} +-- It's now fixed to properly return +-- {1, nil, nil, nil, nil, nil, "seven"} +-- Thanks to "haddock" for catching the error. +-- +-- 20140116.10 The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up. +-- +-- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2", +-- and this caused some problems. +-- +-- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate, +-- and had of course diverged (encode_pretty didn't get the fixes that encode got, so +-- sometimes produced incorrect results; thanks to Mattie for the heads up). +-- +-- Handle encoding tables with non-positive numeric keys (unlikely, but possible). +-- +-- If a table has both numeric and string keys, or its numeric keys are inappropriate +-- (such as being non-positive or infinite), the numeric keys are turned into +-- string keys appropriate for a JSON object. So, as before, +-- JSON:encode({ "one", "two", "three" }) +-- produces the array +-- ["one","two","three"] +-- but now something with mixed key types like +-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) +-- instead of throwing an error produces an object: +-- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} +-- +-- To maintain the prior throw-an-error semantics, set +-- JSON.noKeyConversion = true +-- +-- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry. +-- +-- 20130120.6 Comment update: added a link to the specific page on my blog where this code can +-- be found, so that folks who come across the code outside of my blog can find updates +-- more easily. +-- +-- 20111207.5 Added support for the 'etc' arguments, for better error reporting. +-- +-- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. +-- +-- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: +-- +-- * When encoding lua for JSON, Sparse numeric arrays are now handled by +-- spitting out full arrays, such that +-- JSON:encode({"one", "two", [10] = "ten"}) +-- returns +-- ["one","two",null,null,null,null,null,null,null,"ten"] +-- +-- In 20100810.2 and earlier, only up to the first non-null value would have been retained. +-- +-- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". +-- Version 20100810.2 and earlier created invalid JSON in both cases. +-- +-- * Unicode surrogate pairs are now detected when decoding JSON. +-- +-- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding +-- +-- 20100731.1 initial public release +-- From 3164b8ddaf4bb4d27dae3455b87d83f9270b25a6 Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Tue, 9 Jun 2026 17:30:39 +0200 Subject: [PATCH 09/10] standalone mode compatibility patch for SC --- .../canopsis/canopsis2x-events-apiv2.lua | 15 +++++++++------ .../clickhouse/clickhouse-metrics-apiv2.lua | 15 +++++++++------ .../datadog/datadog-events-apiv2.lua | 15 +++++++++------ .../datadog/datadog-metrics-apiv2.lua | 15 +++++++++------ .../elasticsearch/elastic-events-apiv2.lua | 15 +++++++++------ .../elasticsearch/elastic-metrics-apiv2.lua | 15 +++++++++------ .../google/bigquery-events-apiv2.lua | 15 +++++++++------ .../influxdb/influxdb2-metrics-apiv2.lua | 15 +++++++++------ .../kafka/kafka-events-apiv2.lua | 15 +++++++++------ centreon-certified/keep/keep-events-apiv2.lua | 19 ++++++++++++------- .../logstash/logstash-events-apiv2.lua | 15 +++++++++------ centreon-certified/omi/omi_events-apiv2.lua | 15 +++++++++------ .../opsgenie/opsgenie-events-apiv2.lua | 15 +++++++++------ .../pagerduty/pagerduty-events-apiv2.lua | 15 +++++++++------ .../servicenow/servicenow-em-events-apiv2.lua | 15 +++++++++------ .../servicenow-incident-events-apiv2.lua | 15 +++++++++------ .../signl4/signl4-events-apiv2.lua | 15 +++++++++------ .../splunk/splunk-events-apiv2.lua | 15 +++++++++------ .../splunk/splunk-metrics-apiv2.lua | 15 +++++++++------ 19 files changed, 174 insertions(+), 115 deletions(-) diff --git a/centreon-certified/canopsis/canopsis2x-events-apiv2.lua b/centreon-certified/canopsis/canopsis2x-events-apiv2.lua index cbefe05f..f4b26dc6 100644 --- a/centreon-certified/canopsis/canopsis2x-events-apiv2.lua +++ b/centreon-certified/canopsis/canopsis2x-events-apiv2.lua @@ -29,7 +29,7 @@ end -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -49,13 +49,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/canopsis4-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/canopsis4-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) self.bbdo_version = self.sc_common:get_bbdo_version() @@ -116,6 +116,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -626,7 +627,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/clickhouse/clickhouse-metrics-apiv2.lua b/centreon-certified/clickhouse/clickhouse-metrics-apiv2.lua index adef9db6..f85ba5b1 100644 --- a/centreon-certified/clickhouse/clickhouse-metrics-apiv2.lua +++ b/centreon-certified/clickhouse/clickhouse-metrics-apiv2.lua @@ -24,7 +24,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -45,13 +45,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/clickhouse-metrics.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/clickhouse-metrics.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -88,6 +88,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -426,7 +427,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/datadog/datadog-events-apiv2.lua b/centreon-certified/datadog/datadog-events-apiv2.lua index c4b524a0..eea91171 100644 --- a/centreon-certified/datadog/datadog-events-apiv2.lua +++ b/centreon-certified/datadog/datadog-events-apiv2.lua @@ -23,7 +23,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -42,13 +42,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/datadog-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/datadog-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -81,6 +81,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -292,7 +293,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/datadog/datadog-metrics-apiv2.lua b/centreon-certified/datadog/datadog-metrics-apiv2.lua index 171bab45..568e3f82 100644 --- a/centreon-certified/datadog/datadog-metrics-apiv2.lua +++ b/centreon-certified/datadog/datadog-metrics-apiv2.lua @@ -24,7 +24,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -43,13 +43,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/datadog-metrics.log" - local log_level = params.log_level or 3 + params.logfile = params.logfile or "/var/log/centreon-broker/datadog-metrics.log" + params.log_level = params.log_level or 3 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -86,6 +86,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -340,7 +341,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/elasticsearch/elastic-events-apiv2.lua b/centreon-certified/elasticsearch/elastic-events-apiv2.lua index 2480e01f..b72df028 100644 --- a/centreon-certified/elasticsearch/elastic-events-apiv2.lua +++ b/centreon-certified/elasticsearch/elastic-events-apiv2.lua @@ -22,7 +22,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- event_queue class -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -44,13 +44,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/elastic-events-apiv2.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/elastic-events-apiv2.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -77,6 +77,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -281,7 +282,9 @@ function EventQueue:format_accepted_event() -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/elasticsearch/elastic-metrics-apiv2.lua b/centreon-certified/elasticsearch/elastic-metrics-apiv2.lua index 3bcd35b1..fe128712 100644 --- a/centreon-certified/elasticsearch/elastic-metrics-apiv2.lua +++ b/centreon-certified/elasticsearch/elastic-metrics-apiv2.lua @@ -25,7 +25,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -48,13 +48,13 @@ function EventQueue.new(params) self.fail_message_counter = 0 -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/elastic-metrics.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/elastic-metrics.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -105,6 +105,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -706,7 +707,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/google/bigquery-events-apiv2.lua b/centreon-certified/google/bigquery-events-apiv2.lua index 3acbe1a9..6551ab05 100644 --- a/centreon-certified/google/bigquery-events-apiv2.lua +++ b/centreon-certified/google/bigquery-events-apiv2.lua @@ -11,7 +11,7 @@ local sc_oauth = require("centreon-stream-connectors-lib.google.auth.oauth") local sc_bq = require("centreon-stream-connectors-lib.google.bigquery.bigquery") local curl = require("cURL") -local EventQueue = {} +EventQueue = {} function EventQueue.new(params) local self = {} @@ -27,13 +27,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/stream-connector.log" - local log_level = params.log_level or 2 + params.logfile = params.logfile or "/var/log/centreon-broker/stream-connector.log" + params.log_level = params.log_level or 2 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -109,6 +109,7 @@ function EventQueue.new(params) self.sc_bq = sc_bq.new(self.sc_params.params, self.sc_logger) self.sc_bq:get_tables_schema() self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) -- return EventQueue object setmetatable(self, { __index = EventQueue }) @@ -380,7 +381,9 @@ function EventQueue:call (data, table_name) return true end -local queue +if not queue then + local queue +end function init(params) queue = EventQueue.new(params) diff --git a/centreon-certified/influxdb/influxdb2-metrics-apiv2.lua b/centreon-certified/influxdb/influxdb2-metrics-apiv2.lua index b3ab1b62..c74d41eb 100644 --- a/centreon-certified/influxdb/influxdb2-metrics-apiv2.lua +++ b/centreon-certified/influxdb/influxdb2-metrics-apiv2.lua @@ -24,7 +24,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -46,13 +46,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/infuxdb2-metrics.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/infuxdb2-metrics.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -92,6 +92,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -379,7 +380,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/kafka/kafka-events-apiv2.lua b/centreon-certified/kafka/kafka-events-apiv2.lua index b1b790dd..56d98bd1 100644 --- a/centreon-certified/kafka/kafka-events-apiv2.lua +++ b/centreon-certified/kafka/kafka-events-apiv2.lua @@ -13,7 +13,7 @@ local kafka_producer = require("centreon-stream-connectors-lib.rdkafka.producer" local kafka_topic_config = require("centreon-stream-connectors-lib.rdkafka.topic_config") local kafka_topic = require("centreon-stream-connectors-lib.rdkafka.topic") -local EventQueue = {} +EventQueue = {} function EventQueue.new(params) local self = {} @@ -29,13 +29,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/kafka-stream-connector.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/kafka-stream-connector.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) self.sc_kafka_config = kafka_config.new() self.sc_kafka_topic_config = kafka_topic_config.new() @@ -88,6 +88,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -239,7 +240,9 @@ function EventQueue:call (data) return true end -local queue +if not queue then + local queue +end function init(params) queue = EventQueue.new(params) diff --git a/centreon-certified/keep/keep-events-apiv2.lua b/centreon-certified/keep/keep-events-apiv2.lua index 49f4e37e..82ca0c18 100644 --- a/centreon-certified/keep/keep-events-apiv2.lua +++ b/centreon-certified/keep/keep-events-apiv2.lua @@ -14,6 +14,7 @@ local sc_event = require("centreon-stream-connectors-lib.sc_event") local sc_params = require("centreon-stream-connectors-lib.sc_params") local sc_macros = require("centreon-stream-connectors-lib.sc_macros") local sc_flush = require("centreon-stream-connectors-lib.sc_flush") +local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -------------------------------------------------------------------------------- -- Classe event_queue @@ -23,7 +24,7 @@ local sc_flush = require("centreon-stream-connectors-lib.sc_flush") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -42,13 +43,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/keep-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/keep-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -82,6 +83,8 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) + self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -466,7 +469,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) @@ -493,7 +498,7 @@ function write (event) end end - queue.sc_event = sc_event.new(event, queue.sc_params.params, queue.sc_common, queue.sc_logger, queue.sc_broker) + queue.sc_event = sc_event.new(event, queue.sc_params.params, queue.sc_common, queue.sc_logger, queue.sc_broker, queue.sc_storage) if queue.sc_event:is_valid_category() then if queue.sc_event:is_valid_element() then diff --git a/centreon-certified/logstash/logstash-events-apiv2.lua b/centreon-certified/logstash/logstash-events-apiv2.lua index 508aff68..4c772f04 100644 --- a/centreon-certified/logstash/logstash-events-apiv2.lua +++ b/centreon-certified/logstash/logstash-events-apiv2.lua @@ -19,7 +19,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -39,13 +39,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/logstash-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/logstash-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -76,6 +76,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -278,7 +279,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/omi/omi_events-apiv2.lua b/centreon-certified/omi/omi_events-apiv2.lua index 996df4a1..a19c7533 100644 --- a/centreon-certified/omi/omi_events-apiv2.lua +++ b/centreon-certified/omi/omi_events-apiv2.lua @@ -49,7 +49,7 @@ local previous_event = "" -- EventQueue class -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -68,13 +68,13 @@ function EventQueue.new(params) } -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/omi_event.log" - local log_level = params.log_level or 2 + params.logfile = params.logfile or "/var/log/centreon-broker/omi_event.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -109,6 +109,7 @@ function EventQueue.new(params) self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -307,7 +308,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/opsgenie/opsgenie-events-apiv2.lua b/centreon-certified/opsgenie/opsgenie-events-apiv2.lua index cfe3c1ea..9d177024 100644 --- a/centreon-certified/opsgenie/opsgenie-events-apiv2.lua +++ b/centreon-certified/opsgenie/opsgenie-events-apiv2.lua @@ -23,7 +23,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -42,13 +42,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/opsgenie-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/opsgenie-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -94,6 +94,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -419,7 +420,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/pagerduty/pagerduty-events-apiv2.lua b/centreon-certified/pagerduty/pagerduty-events-apiv2.lua index 0009db9b..abc181cb 100644 --- a/centreon-certified/pagerduty/pagerduty-events-apiv2.lua +++ b/centreon-certified/pagerduty/pagerduty-events-apiv2.lua @@ -24,7 +24,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -43,13 +43,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/pagerduty-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/pagerduty-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -83,6 +83,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -414,7 +415,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/servicenow/servicenow-em-events-apiv2.lua b/centreon-certified/servicenow/servicenow-em-events-apiv2.lua index fc52538f..c95dad32 100644 --- a/centreon-certified/servicenow/servicenow-em-events-apiv2.lua +++ b/centreon-certified/servicenow/servicenow-em-events-apiv2.lua @@ -21,7 +21,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- EventQueue class -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -48,13 +48,13 @@ function EventQueue.new (params) self.events = {} self.fail = false - local logfile = params.logfile or "/var/log/centreon-broker/servicenow-em-stream-connector.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/servicenow-em-stream-connector.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) self.sc_params.params.instance = params.instance @@ -87,6 +87,7 @@ function EventQueue.new (params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -370,7 +371,9 @@ function EventQueue:format_event_service() end end -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/servicenow/servicenow-incident-events-apiv2.lua b/centreon-certified/servicenow/servicenow-incident-events-apiv2.lua index 634d6db8..6379da59 100644 --- a/centreon-certified/servicenow/servicenow-incident-events-apiv2.lua +++ b/centreon-certified/servicenow/servicenow-incident-events-apiv2.lua @@ -21,7 +21,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- EventQueue class -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -48,13 +48,13 @@ function EventQueue.new (params) self.events = {} self.fail = false - local logfile = params.logfile or "/var/log/centreon-broker/servicenow-incident-stream-connector.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/servicenow-incident-stream-connector.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) self.sc_params.params.instance = params.instance @@ -97,6 +97,7 @@ function EventQueue.new (params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -379,7 +380,9 @@ function EventQueue:format_event_service() } end -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/signl4/signl4-events-apiv2.lua b/centreon-certified/signl4/signl4-events-apiv2.lua index 7c0d52aa..3e6a7e13 100644 --- a/centreon-certified/signl4/signl4-events-apiv2.lua +++ b/centreon-certified/signl4/signl4-events-apiv2.lua @@ -21,7 +21,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- event_queue class -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -40,13 +40,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/signl4-events-apiv2.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/signl4-events-apiv2.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -78,6 +78,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -277,7 +278,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/splunk/splunk-events-apiv2.lua b/centreon-certified/splunk/splunk-events-apiv2.lua index ea3fe2c9..7de1e155 100644 --- a/centreon-certified/splunk/splunk-events-apiv2.lua +++ b/centreon-certified/splunk/splunk-events-apiv2.lua @@ -19,7 +19,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -39,13 +39,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/splunk-events.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/splunk-events.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -76,6 +76,7 @@ function EventQueue.new(params) self.sc_params:build_accepted_elements_info() self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -275,7 +276,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) diff --git a/centreon-certified/splunk/splunk-metrics-apiv2.lua b/centreon-certified/splunk/splunk-metrics-apiv2.lua index ebb69dd2..bbcad839 100644 --- a/centreon-certified/splunk/splunk-metrics-apiv2.lua +++ b/centreon-certified/splunk/splunk-metrics-apiv2.lua @@ -17,7 +17,7 @@ local sc_storage = require("centreon-stream-connectors-lib.sc_storage") -- Classe event_queue -------------------------------------------------------------------------------- -local EventQueue = {} +EventQueue = {} EventQueue.__index = EventQueue -------------------------------------------------------------------------------- @@ -37,13 +37,13 @@ function EventQueue.new(params) self.fail = false -- set up log configuration - local logfile = params.logfile or "/var/log/centreon-broker/splunk-metrics.log" - local log_level = params.log_level or 1 + params.logfile = params.logfile or "/var/log/centreon-broker/splunk-metrics.log" + params.log_level = params.log_level or 1 + params.logger_backend = params.logger_backend or "broker" -- initiate mandatory objects - self.sc_logger = sc_logger.new(logfile, log_level) + self.sc_logger = sc_logger.new(params) self.sc_common = sc_common.new(self.sc_logger) - self.sc_broker = sc_broker.new(self.sc_logger) self.sc_params = sc_params.new(self.sc_common, self.sc_logger) -- checking mandatory parameters and setting a fail flag @@ -81,6 +81,7 @@ function EventQueue.new(params) self.sc_flush = sc_flush.new(self.sc_params.params, self.sc_logger) self.sc_storage = sc_storage.new(self.sc_common, self.sc_logger, self.sc_params.params) + self.sc_broker = sc_broker.new(self.sc_params.params, self.sc_logger) local categories = self.sc_params.params.bbdo.categories local elements = self.sc_params.params.bbdo.elements @@ -412,7 +413,9 @@ end -- Required functions for Broker StreamConnector -------------------------------------------------------------------------------- -local queue +if not queue then + local queue +end -- Fonction init() function init(conf) From e495a69c7a6d3bec8fdaf99edea25a5cf5fc1d4b Mon Sep 17 00:00:00 2001 From: tanguyvda Date: Wed, 10 Jun 2026 11:05:01 +0200 Subject: [PATCH 10/10] convert userdata cache to tables --- .../standalone/standalone-events-apiv2.lua | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/centreon-certified/standalone/standalone-events-apiv2.lua b/centreon-certified/standalone/standalone-events-apiv2.lua index 5bb1e727..d1fc57a3 100644 --- a/centreon-certified/standalone/standalone-events-apiv2.lua +++ b/centreon-certified/standalone/standalone-events-apiv2.lua @@ -128,13 +128,53 @@ function EventQueue:format_accepted_event() end function EventQueue:format_event_host() - -- nothing to do - return true + self:fix_cache_tables() end function EventQueue:format_event_service() - -- nothing to do - return true + self:fix_cache_tables() +end + +--[[ + depending on the broker version and probably bbdo protocol, some cache data are not tables but userdata + need to convert them into table to make sure that this stream connector will be able to json encode the cache part of the event +]] +function EventQueue:fix_cache_tables() + local event_cache = self.sc_event.event.cache + + if event_cache.host then + self:rebuild_cache("host") + end + + if event_cache.service then + self:rebuild_cache("service") + end + + if event_cache.hostgroups then + self:rebuild_cache("hostgroups") + end + + if event_cache.servicegroups then + self:rebuild_cache("servicegroups") + end + + if event_cache.ba then + self:rebuild_cache("ba") + end + + if event_cache.bvs then + self:rebuild_cache("bvs") + end +end + +function EventQueue:rebuild_cache(cache_type) + if type(self.sc_event.event.cache[cache_type]) == "userdata" then + local temp_cache_table = {[cache_type] = {}} + for index, value in pairs(self.sc_event.event.cache[cache_type]) do + temp_cache_table[cache_type][index] = value + end + self.sc_event.event.cache[cache_type] = temp_cache_table[cache_type] + end end --------------------------------------------------------------------------------