Initial commit: mod_reqin_log Apache module

Features:
- JSON logging of HTTP requests to Unix domain socket
- Configurable HTTP headers logging (flat JSON structure)
- Header value truncation and count limits
- Automatic reconnect on socket disconnection
- Error reporting with throttling

Configuration directives:
- JsonSockLogEnabled: Enable/disable logging
- JsonSockLogSocket: Unix socket path
- JsonSockLogHeaders: List of headers to log
- JsonSockLogMaxHeaders: Maximum headers to log
- JsonSockLogMaxHeaderValueLen: Max header value length
- JsonSockLogReconnectInterval: Reconnect delay
- JsonSockLogErrorReportInterval: Error log throttle

Includes:
- Module source code (src/)
- Unit and integration tests (tests/, scripts/)
- Documentation (README.md, architecture.yml)
- Build configuration (CMakeLists.txt, Makefile)
- Packaging (deb/rpm)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-02-26 13:55:07 +01:00
commit 66549acf5c
27 changed files with 3550 additions and 0 deletions

614
src/mod_reqin_log.c Normal file
View File

@ -0,0 +1,614 @@
/*
* mod_reqin_log.c - Apache HTTPD module for logging HTTP requests as JSON to Unix socket
*
* Copyright (c) 2026. All rights reserved.
*/
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"
#include "http_request.h"
#include "apr_strings.h"
#include "apr_time.h"
#include "apr_lib.h"
#include "ap_config.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#define MOD_REQIN_LOG_NAME "mod_reqin_log"
/* Default configuration values */
#define DEFAULT_MAX_HEADERS 10
#define DEFAULT_MAX_HEADER_VALUE_LEN 256
#define DEFAULT_RECONNECT_INTERVAL 10
#define DEFAULT_ERROR_REPORT_INTERVAL 10
/* Module configuration structure */
typedef struct {
int enabled;
const char *socket_path;
apr_array_header_t *headers;
int max_headers;
int max_header_value_len;
int reconnect_interval;
int error_report_interval;
} reqin_log_config_t;
/* Dynamic string buffer */
typedef struct {
char *data;
apr_size_t len;
apr_size_t capacity;
apr_pool_t *pool;
} dynbuf_t;
/* Per-child process state */
typedef struct {
int socket_fd;
apr_time_t last_connect_attempt;
apr_time_t last_error_report;
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
};
/* 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 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);
/* Forward declarations for hooks */
static int reqin_log_post_read_request(request_rec *r);
static void reqin_log_child_init(apr_pool_t *p, server_rec *s);
static void reqin_log_register_hooks(apr_pool_t *p);
/* Command table */
static const command_rec reqin_log_cmds[] = {
AP_INIT_FLAG("JsonSockLogEnabled", cmd_set_enabled, NULL, RSRC_CONF,
"Enable or disable mod_reqin_log (On|Off)"),
AP_INIT_TAKE1("JsonSockLogSocket", cmd_set_socket, NULL, RSRC_CONF,
"Unix domain socket path for JSON logging"),
AP_INIT_ITERATE("JsonSockLogHeaders", cmd_set_headers, NULL, RSRC_CONF,
"List of HTTP header names to log"),
AP_INIT_TAKE1("JsonSockLogMaxHeaders", cmd_set_max_headers, NULL, RSRC_CONF,
"Maximum number of headers to log (default: 10)"),
AP_INIT_TAKE1("JsonSockLogMaxHeaderValueLen", cmd_set_max_header_value_len, NULL, RSRC_CONF,
"Maximum length of header value to log (default: 256)"),
AP_INIT_TAKE1("JsonSockLogReconnectInterval", cmd_set_reconnect_interval, NULL, RSRC_CONF,
"Reconnect interval in seconds (default: 10)"),
AP_INIT_TAKE1("JsonSockLogErrorReportInterval", cmd_set_error_report_interval, NULL, RSRC_CONF,
"Error report interval in seconds (default: 10)"),
{ NULL }
};
/* 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 */
};
/* Get module configuration */
static reqin_log_config_t *get_module_config(server_rec *s)
{
reqin_log_config_t *cfg = (reqin_log_config_t *)ap_get_module_config(s->module_config, &reqin_log_module);
return cfg;
}
/* ============== Dynamic Buffer Functions ============== */
static void dynbuf_init(dynbuf_t *db, apr_pool_t *pool, apr_size_t initial_capacity)
{
db->pool = pool;
db->capacity = initial_capacity;
db->len = 0;
db->data = apr_palloc(pool, initial_capacity);
db->data[0] = '\0';
}
static void dynbuf_append(dynbuf_t *db, const char *str, apr_size_t len)
{
if (str == NULL) return;
if (len == (apr_size_t)-1) {
len = strlen(str);
}
if (db->len + len >= db->capacity) {
apr_size_t new_capacity = (db->len + len + 1) * 2;
char *new_data = apr_palloc(db->pool, new_capacity);
memcpy(new_data, db->data, db->len);
db->data = new_data;
db->capacity = new_capacity;
}
memcpy(db->data + db->len, str, len);
db->len += len;
db->data[db->len] = '\0';
}
static void dynbuf_append_char(dynbuf_t *db, char c)
{
if (db->len + 1 >= db->capacity) {
apr_size_t new_capacity = db->capacity * 2;
char *new_data = apr_palloc(db->pool, new_capacity);
memcpy(new_data, db->data, db->len);
db->data = new_data;
db->capacity = new_capacity;
}
db->data[db->len++] = c;
db->data[db->len] = '\0';
}
/* ============== JSON Helper Functions ============== */
static void append_json_string(dynbuf_t *db, const char *str)
{
if (str == NULL) {
return;
}
for (const char *p = str; *p; p++) {
char c = *p;
switch (c) {
case '"': dynbuf_append(db, "\\\"", 2); break;
case '\\': dynbuf_append(db, "\\\\", 2); break;
case '\b': dynbuf_append(db, "\\b", 2); break;
case '\f': dynbuf_append(db, "\\f", 2); break;
case '\n': dynbuf_append(db, "\\n", 2); break;
case '\r': dynbuf_append(db, "\\r", 2); break;
case '\t': dynbuf_append(db, "\\t", 2); break;
default:
if ((unsigned char)c < 0x20) {
char unicode[8];
snprintf(unicode, sizeof(unicode), "\\u%04x", (unsigned char)c);
dynbuf_append(db, unicode, -1);
} else {
dynbuf_append_char(db, c);
}
break;
}
}
}
static void format_iso8601(dynbuf_t *db, apr_time_t t)
{
apr_time_exp_t tm;
apr_time_exp_gmt(&tm, t);
char time_str[32];
snprintf(time_str, sizeof(time_str), "%04d-%02d-%02dT%02d:%02d:%02dZ",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
dynbuf_append(db, time_str, -1);
}
/* ============== Configuration Command Handlers ============== */
static const char *cmd_set_enabled(cmd_parms *cmd, void *cfg, 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);
}
conf->enabled = flag ? 1 : 0;
return NULL;
}
static const char *cmd_set_socket(cmd_parms *cmd, void *cfg, 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);
}
conf->socket_path = apr_pstrdup(cmd->pool, arg);
return NULL;
}
static const char *cmd_set_headers(cmd_parms *cmd, void *cfg, 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);
}
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);
return NULL;
}
static const char *cmd_set_max_headers(cmd_parms *cmd, void *cfg, 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);
}
int val = atoi(arg);
if (val < 0) {
return "JsonSockLogMaxHeaders must be >= 0";
}
conf->max_headers = val;
return NULL;
}
static const char *cmd_set_max_header_value_len(cmd_parms *cmd, void *cfg, 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);
}
int val = atoi(arg);
if (val < 1) {
return "JsonSockLogMaxHeaderValueLen must be >= 1";
}
conf->max_header_value_len = val;
return NULL;
}
static const char *cmd_set_reconnect_interval(cmd_parms *cmd, void *cfg, 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);
}
int val = atoi(arg);
if (val < 0) {
return "JsonSockLogReconnectInterval must be >= 0";
}
conf->reconnect_interval = val;
return NULL;
}
static const char *cmd_set_error_report_interval(cmd_parms *cmd, void *cfg, 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);
}
int val = atoi(arg);
if (val < 0) {
return "JsonSockLogErrorReportInterval must be >= 0";
}
conf->error_report_interval = val;
return NULL;
}
/* ============== Socket Functions ============== */
static int try_connect(reqin_log_config_t *cfg, 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) {
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) {
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);
}
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));
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)) {
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;
}
return -1;
}
}
g_child_state.connect_failed = 0;
return 0;
}
static int ensure_connected(reqin_log_config_t *cfg, server_rec *s)
{
if (g_child_state.socket_fd >= 0 && !g_child_state.connect_failed) {
return 0;
}
return try_connect(cfg, s);
}
static int write_to_socket(const char *data, apr_size_t len, server_rec *s, reqin_log_config_t *cfg)
{
if (g_child_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);
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;
apr_time_t now = apr_time_now();
if ((now - g_child_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;
}
return -1;
}
return -1;
}
total_written += n;
}
return 0;
}
/* ============== Request Logging Functions ============== */
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;
}
}
return NULL;
}
static void log_request(request_rec *r, reqin_log_config_t *cfg)
{
apr_pool_t *pool = r->pool;
server_rec *s = r->server;
char port_buf[16];
if (ensure_connected(cfg, s) < 0) {
return;
}
dynbuf_t buf;
dynbuf_init(&buf, pool, 4096);
dynbuf_append(&buf, "{", 1);
/* time */
dynbuf_append(&buf, "\"time\":\"", 8);
format_iso8601(&buf, r->request_time);
dynbuf_append(&buf, "\",", 2);
/* timestamp */
apr_time_t now = apr_time_now();
apr_uint64_t ns = (apr_uint64_t)now * 1000;
char ts_buf[32];
snprintf(ts_buf, sizeof(ts_buf), "%" APR_UINT64_T_FMT, ns);
dynbuf_append(&buf, "\"timestamp\":", 12);
dynbuf_append(&buf, ts_buf, -1);
dynbuf_append(&buf, ",", 1);
/* src_ip */
dynbuf_append(&buf, "\"src_ip\":\"", 10);
dynbuf_append(&buf, r->useragent_ip ? r->useragent_ip : r->connection->client_ip, -1);
dynbuf_append(&buf, "\",", 2);
/* src_port */
port_buf[0] = '\0';
if (r->connection->client_addr != NULL) {
snprintf(port_buf, sizeof(port_buf), "%u", r->connection->client_addr->port);
}
dynbuf_append(&buf, "\"src_port\":", 11);
dynbuf_append(&buf, port_buf, -1);
dynbuf_append(&buf, ",", 1);
/* dst_ip */
dynbuf_append(&buf, "\"dst_ip\":\"", 10);
dynbuf_append(&buf, r->connection->local_ip, -1);
dynbuf_append(&buf, "\",", 2);
/* dst_port */
port_buf[0] = '\0';
if (r->connection->local_addr != NULL) {
snprintf(port_buf, sizeof(port_buf), "%u", r->connection->local_addr->port);
}
dynbuf_append(&buf, "\"dst_port\":", 11);
dynbuf_append(&buf, port_buf, -1);
dynbuf_append(&buf, ",", 1);
/* method */
dynbuf_append(&buf, "\"method\":\"", 10);
append_json_string(&buf, r->method);
dynbuf_append(&buf, "\",", 2);
/* path */
dynbuf_append(&buf, "\"path\":\"", 8);
append_json_string(&buf, r->parsed_uri.path ? r->parsed_uri.path : "/");
dynbuf_append(&buf, "\",", 2);
/* host */
const char *host = apr_table_get(r->headers_in, "Host");
dynbuf_append(&buf, "\"host\":\"", 8);
append_json_string(&buf, host ? host : "");
dynbuf_append(&buf, "\",", 2);
/* http_version */
dynbuf_append(&buf, "\"http_version\":\"", 16);
dynbuf_append(&buf, r->protocol, -1);
dynbuf_append(&buf, "\"", 1);
/* headers - flat structure at same level as other fields */
if (cfg->headers && cfg->headers->nelts > 0) {
int header_count = 0;
int max_to_log = cfg->max_headers;
const char **header_names = (const char **)cfg->headers->elts;
for (int i = 0; i < cfg->headers->nelts && header_count < max_to_log; i++) {
const char *header_name = header_names[i];
const char *header_value = get_header(r, header_name);
if (header_value != NULL) {
dynbuf_append(&buf, ",\"header_", 9);
append_json_string(&buf, header_name);
dynbuf_append(&buf, "\":\"", 3);
apr_size_t val_len = strlen(header_value);
if ((int)val_len > cfg->max_header_value_len) {
val_len = cfg->max_header_value_len;
}
char *truncated = apr_pstrmemdup(pool, header_value, val_len);
append_json_string(&buf, truncated);
dynbuf_append(&buf, "\"", 1);
header_count++;
}
}
}
dynbuf_append(&buf, "}\n", 2);
write_to_socket(buf.data, buf.len, s, cfg);
}
/* ============== 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) {
return DECLINED;
}
log_request(r, cfg);
return DECLINED;
}
static void reqin_log_child_init(apr_pool_t *p, server_rec *s)
{
(void)p;
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) {
return;
}
try_connect(cfg, s);
}
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);
}