Refactor: thread-safe per-process state and add tests

Major changes:
- Move child state from global variable to server config (reqin_log_server_conf_t)
- Add reqin_log_create_server_conf() for proper per-server initialization
- Fix thread safety for worker/event MPMs
- Add cmocka unit tests (test_module_real.c)
- Add Python integration tests (test_integration.py)
- Update CI workflow and Dockerfiles for test execution
- Fix: Remove child_exit hook (not in architecture.yml)

Tests:
- Unit tests: JSON escaping, ISO8601 formatting, header truncation
- Integration tests: basic_logging, header_limits, socket_unavailable, socket_loss

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-02-26 23:28:45 +01:00
parent 7cfd14fb65
commit 070c2a7bd2
7 changed files with 1425 additions and 340 deletions

View File

@ -52,7 +52,7 @@ typedef struct {
apr_pool_t *pool;
} dynbuf_t;
/* Per-child process state */
/* Per-child process state - stored in server config */
typedef struct {
int socket_fd;
apr_time_t last_connect_attempt;
@ -60,27 +60,28 @@ typedef struct {
int connect_failed;
} reqin_log_child_state_t;
/* Global child state (one per process) */
static reqin_log_child_state_t g_child_state = {
.socket_fd = -1,
.last_connect_attempt = 0,
.last_error_report = 0,
.connect_failed = 0
};
/* Module server configuration structure */
typedef struct {
reqin_log_config_t *config;
reqin_log_child_state_t child_state;
} reqin_log_server_conf_t;
/* Forward declarations for helper functions */
static void dynbuf_append(dynbuf_t *db, const char *str, apr_size_t len);
static void append_json_string(dynbuf_t *db, const char *str);
static void format_iso8601(dynbuf_t *db, apr_time_t t);
/* Forward declarations for server config */
static void *reqin_log_create_server_conf(apr_pool_t *pool, server_rec *s);
/* Forward declarations for commands */
static const char *cmd_set_enabled(cmd_parms *cmd, void *cfg, int flag);
static const char *cmd_set_socket(cmd_parms *cmd, void *cfg, const char *arg);
static const char *cmd_set_headers(cmd_parms *cmd, void *cfg, const char *arg);
static const char *cmd_set_max_headers(cmd_parms *cmd, void *cfg, const char *arg);
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *cfg, const char *arg);
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *cfg, const char *arg);
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *cfg, const char *arg);
static const char *cmd_set_enabled(cmd_parms *cmd, void *dummy, int flag);
static const char *cmd_set_socket(cmd_parms *cmd, void *dummy, const char *arg);
static const char *cmd_set_headers(cmd_parms *cmd, void *dummy, const char *arg);
static const char *cmd_set_max_headers(cmd_parms *cmd, void *dummy, const char *arg);
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *dummy, const char *arg);
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *dummy, const char *arg);
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *dummy, const char *arg);
/* Forward declarations for hooks */
static int reqin_log_post_read_request(request_rec *r);
@ -109,19 +110,40 @@ static const command_rec reqin_log_cmds[] = {
/* Module definition */
module AP_MODULE_DECLARE_DATA reqin_log_module = {
STANDARD20_MODULE_STUFF,
NULL, /* per-directory config creator */
NULL, /* dir config merger */
NULL, /* server config creator */
NULL, /* server config merger */
reqin_log_cmds, /* command table */
reqin_log_register_hooks /* register hooks */
NULL, /* per-directory config creator */
NULL, /* dir config merger */
reqin_log_create_server_conf, /* server config creator */
NULL, /* server config merger */
reqin_log_cmds, /* command table */
reqin_log_register_hooks /* register hooks */
};
/* Get module configuration */
static reqin_log_config_t *get_module_config(server_rec *s)
static reqin_log_server_conf_t *get_server_conf(server_rec *s)
{
reqin_log_config_t *cfg = (reqin_log_config_t *)ap_get_module_config(s->module_config, &reqin_log_module);
return cfg;
reqin_log_server_conf_t *srv_conf = (reqin_log_server_conf_t *)ap_get_module_config(s->module_config, &reqin_log_module);
return srv_conf;
}
/* Create server configuration */
static void *reqin_log_create_server_conf(apr_pool_t *pool, server_rec *s)
{
(void)s;
reqin_log_server_conf_t *srv_conf = apr_pcalloc(pool, sizeof(reqin_log_server_conf_t));
srv_conf->config = apr_pcalloc(pool, sizeof(reqin_log_config_t));
srv_conf->config->headers = apr_array_make(pool, 5, sizeof(const char *));
srv_conf->config->max_headers = DEFAULT_MAX_HEADERS;
srv_conf->config->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
srv_conf->config->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
srv_conf->config->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
srv_conf->child_state.socket_fd = -1;
srv_conf->child_state.last_connect_attempt = 0;
srv_conf->child_state.last_error_report = 0;
srv_conf->child_state.connect_failed = 0;
return srv_conf;
}
/* ============== Dynamic Buffer Functions ============== */
@ -214,227 +236,184 @@ static void format_iso8601(dynbuf_t *db, apr_time_t t)
/* ============== Configuration Command Handlers ============== */
static const char *cmd_set_enabled(cmd_parms *cmd, void *cfg, int flag)
static const char *cmd_set_enabled(cmd_parms *cmd, void *dummy, int flag)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
conf->max_headers = DEFAULT_MAX_HEADERS;
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
conf->enabled = flag ? 1 : 0;
srv_conf->config->enabled = flag ? 1 : 0;
return NULL;
}
static const char *cmd_set_socket(cmd_parms *cmd, void *cfg, const char *arg)
static const char *cmd_set_socket(cmd_parms *cmd, void *dummy, const char *arg)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->enabled = 0;
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
conf->max_headers = DEFAULT_MAX_HEADERS;
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
conf->socket_path = apr_pstrdup(cmd->pool, arg);
srv_conf->config->socket_path = apr_pstrdup(cmd->pool, arg);
return NULL;
}
static const char *cmd_set_headers(cmd_parms *cmd, void *cfg, const char *arg)
static const char *cmd_set_headers(cmd_parms *cmd, void *dummy, const char *arg)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->enabled = 0;
conf->socket_path = NULL;
conf->max_headers = DEFAULT_MAX_HEADERS;
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
if (conf->headers == NULL) {
conf->headers = apr_array_make(cmd->pool, 5, sizeof(const char *));
}
*(const char **)apr_array_push(conf->headers) = apr_pstrdup(cmd->pool, arg);
*(const char **)apr_array_push(srv_conf->config->headers) = apr_pstrdup(cmd->pool, arg);
return NULL;
}
static const char *cmd_set_max_headers(cmd_parms *cmd, void *cfg, const char *arg)
static const char *cmd_set_max_headers(cmd_parms *cmd, void *dummy, const char *arg)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->enabled = 0;
conf->socket_path = NULL;
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
int val = atoi(arg);
if (val < 0) {
return "JsonSockLogMaxHeaders must be >= 0";
}
conf->max_headers = val;
srv_conf->config->max_headers = val;
return NULL;
}
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *cfg, const char *arg)
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *dummy, const char *arg)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->enabled = 0;
conf->socket_path = NULL;
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
conf->max_headers = DEFAULT_MAX_HEADERS;
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
int val = atoi(arg);
if (val < 1) {
return "JsonSockLogMaxHeaderValueLen must be >= 1";
}
conf->max_header_value_len = val;
srv_conf->config->max_header_value_len = val;
return NULL;
}
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *cfg, const char *arg)
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *dummy, const char *arg)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->enabled = 0;
conf->socket_path = NULL;
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
conf->max_headers = DEFAULT_MAX_HEADERS;
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
conf->error_report_interval = DEFAULT_ERROR_REPORT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
int val = atoi(arg);
if (val < 0) {
return "JsonSockLogReconnectInterval must be >= 0";
}
conf->reconnect_interval = val;
srv_conf->config->reconnect_interval = val;
return NULL;
}
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *cfg, const char *arg)
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *dummy, const char *arg)
{
reqin_log_config_t *conf = get_module_config(cmd->server);
if (conf == NULL) {
conf = apr_pcalloc(cmd->pool, sizeof(reqin_log_config_t));
conf->enabled = 0;
conf->socket_path = NULL;
conf->headers = apr_array_make(cmd->pool, 0, sizeof(const char *));
conf->max_headers = DEFAULT_MAX_HEADERS;
conf->max_header_value_len = DEFAULT_MAX_HEADER_VALUE_LEN;
conf->reconnect_interval = DEFAULT_RECONNECT_INTERVAL;
ap_set_module_config(cmd->server->module_config, &reqin_log_module, conf);
(void)dummy;
reqin_log_server_conf_t *srv_conf = get_server_conf(cmd->server);
if (srv_conf == NULL) {
return "Internal error: server configuration not available";
}
int val = atoi(arg);
if (val < 0) {
return "JsonSockLogErrorReportInterval must be >= 0";
}
conf->error_report_interval = val;
srv_conf->config->error_report_interval = val;
return NULL;
}
/* ============== Socket Functions ============== */
static int try_connect(reqin_log_config_t *cfg, server_rec *s)
static int try_connect(reqin_log_config_t *cfg, reqin_log_child_state_t *state, server_rec *s)
{
apr_time_t now = apr_time_now();
apr_time_t interval = apr_time_from_sec(cfg->reconnect_interval);
if (g_child_state.connect_failed &&
(now - g_child_state.last_connect_attempt) < interval) {
if (state->connect_failed &&
(now - state->last_connect_attempt) < interval) {
return -1;
}
g_child_state.last_connect_attempt = now;
if (g_child_state.socket_fd < 0) {
g_child_state.socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (g_child_state.socket_fd < 0) {
state->last_connect_attempt = now;
if (state->socket_fd < 0) {
state->socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (state->socket_fd < 0) {
ap_log_error(APLOG_MARK, APLOG_ERR, errno, s,
MOD_REQIN_LOG_NAME ": Failed to create socket");
return -1;
}
int flags = fcntl(g_child_state.socket_fd, F_GETFL, 0);
fcntl(g_child_state.socket_fd, F_SETFL, flags | O_NONBLOCK);
int flags = fcntl(state->socket_fd, F_GETFL, 0);
fcntl(state->socket_fd, F_SETFL, flags | O_NONBLOCK);
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", cfg->socket_path);
int rc = connect(g_child_state.socket_fd, (struct sockaddr *)&addr, sizeof(addr));
int rc = connect(state->socket_fd, (struct sockaddr *)&addr, sizeof(addr));
if (rc < 0) {
int err = errno;
if (err != EINPROGRESS && err != EAGAIN && err != EWOULDBLOCK) {
close(g_child_state.socket_fd);
g_child_state.socket_fd = -1;
g_child_state.connect_failed = 1;
if ((now - g_child_state.last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) {
close(state->socket_fd);
state->socket_fd = -1;
state->connect_failed = 1;
if ((now - state->last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) {
ap_log_error(APLOG_MARK, APLOG_ERR, err, s,
MOD_REQIN_LOG_NAME ": Unix socket connect failed: %s", cfg->socket_path);
g_child_state.last_error_report = now;
state->last_error_report = now;
}
return -1;
}
}
g_child_state.connect_failed = 0;
state->connect_failed = 0;
return 0;
}
static int ensure_connected(reqin_log_config_t *cfg, server_rec *s)
static int ensure_connected(reqin_log_config_t *cfg, reqin_log_child_state_t *state, server_rec *s)
{
if (g_child_state.socket_fd >= 0 && !g_child_state.connect_failed) {
if (state->socket_fd >= 0 && !state->connect_failed) {
return 0;
}
return try_connect(cfg, s);
return try_connect(cfg, state, s);
}
static int write_to_socket(const char *data, apr_size_t len, server_rec *s, reqin_log_config_t *cfg)
static int write_to_socket(const char *data, apr_size_t len, server_rec *s,
reqin_log_config_t *cfg, reqin_log_child_state_t *state)
{
if (g_child_state.socket_fd < 0) {
if (state->socket_fd < 0) {
return -1;
}
apr_size_t total_written = 0;
while (total_written < len) {
ssize_t n = write(g_child_state.socket_fd, data + total_written, len - total_written);
ssize_t n = write(state->socket_fd, data + total_written, len - total_written);
if (n < 0) {
int err = errno;
if (err == EAGAIN || err == EWOULDBLOCK) {
return -1;
}
if (err == EPIPE || err == ECONNRESET) {
close(g_child_state.socket_fd);
g_child_state.socket_fd = -1;
g_child_state.connect_failed = 1;
close(state->socket_fd);
state->socket_fd = -1;
state->connect_failed = 1;
apr_time_t now = apr_time_now();
if ((now - g_child_state.last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) {
if ((now - state->last_error_report) >= apr_time_from_sec(cfg->error_report_interval)) {
ap_log_error(APLOG_MARK, APLOG_ERR, err, s,
MOD_REQIN_LOG_NAME ": Unix socket write failed: %s", strerror(err));
g_child_state.last_error_report = now;
state->last_error_report = now;
}
return -1;
}
@ -442,7 +421,7 @@ static int write_to_socket(const char *data, apr_size_t len, server_rec *s, reqi
}
total_written += n;
}
return 0;
}
@ -453,7 +432,7 @@ static const char *get_header(request_rec *r, const char *name)
const apr_table_t *headers = r->headers_in;
apr_table_entry_t *elts = (apr_table_entry_t *)apr_table_elts(headers)->elts;
int nelts = apr_table_elts(headers)->nelts;
for (int i = 0; i < nelts; i++) {
if (strcasecmp(elts[i].key, name) == 0) {
return elts[i].val;
@ -462,13 +441,13 @@ static const char *get_header(request_rec *r, const char *name)
return NULL;
}
static void log_request(request_rec *r, reqin_log_config_t *cfg)
static void log_request(request_rec *r, reqin_log_config_t *cfg, reqin_log_child_state_t *state)
{
apr_pool_t *pool = r->pool;
server_rec *s = r->server;
char port_buf[16];
if (ensure_connected(cfg, s) < 0) {
if (ensure_connected(cfg, state, s) < 0) {
return;
}
@ -571,50 +550,45 @@ static void log_request(request_rec *r, reqin_log_config_t *cfg)
dynbuf_append(&buf, "}\n", 2);
write_to_socket(buf.data, buf.len, s, cfg);
write_to_socket(buf.data, buf.len, s, cfg, state);
}
/* ============== Apache Hooks ============== */
static int reqin_log_post_read_request(request_rec *r)
{
reqin_log_config_t *cfg = get_module_config(r->server);
if (cfg == NULL || !cfg->enabled || cfg->socket_path == NULL) {
reqin_log_server_conf_t *srv_conf = get_server_conf(r->server);
if (srv_conf == NULL || srv_conf->config == NULL ||
!srv_conf->config->enabled || srv_conf->config->socket_path == NULL) {
return DECLINED;
}
log_request(r, cfg);
log_request(r, srv_conf->config, &srv_conf->child_state);
return DECLINED;
}
static void reqin_log_child_init(apr_pool_t *p, server_rec *s)
{
(void)p;
reqin_log_server_conf_t *srv_conf = get_server_conf(s);
reqin_log_config_t *cfg = get_module_config(s);
g_child_state.socket_fd = -1;
g_child_state.last_connect_attempt = 0;
g_child_state.last_error_report = 0;
g_child_state.connect_failed = 0;
if (cfg == NULL || !cfg->enabled || cfg->socket_path == NULL) {
if (srv_conf == NULL) {
return;
}
try_connect(cfg, s);
}
/* Initialize child state for this process */
srv_conf->child_state.socket_fd = -1;
srv_conf->child_state.last_connect_attempt = 0;
srv_conf->child_state.last_error_report = 0;
srv_conf->child_state.connect_failed = 0;
static void reqin_log_child_exit(apr_pool_t *p, server_rec *s)
{
(void)p;
(void)s;
if (g_child_state.socket_fd >= 0) {
close(g_child_state.socket_fd);
g_child_state.socket_fd = -1;
if (srv_conf->config == NULL || !srv_conf->config->enabled ||
srv_conf->config->socket_path == NULL) {
return;
}
try_connect(srv_conf->config, &srv_conf->child_state, s);
}
static void reqin_log_register_hooks(apr_pool_t *p)
@ -622,5 +596,4 @@ static void reqin_log_register_hooks(apr_pool_t *p)
(void)p;
ap_hook_post_read_request(reqin_log_post_read_request, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_child_init(reqin_log_child_init, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_child_exit(reqin_log_child_exit, NULL, NULL, APR_HOOK_MIDDLE);
}