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:
202
architecture.yml
202
architecture.yml
@ -40,26 +40,38 @@ module:
|
||||
- name: register_hooks
|
||||
responsibilities:
|
||||
- Register post_read_request hook for logging at request reception.
|
||||
- Initialize per-process Unix socket connection if enabled.
|
||||
- Register child_init hook for per-process state initialization.
|
||||
- Initialize per-process server configuration structure.
|
||||
- name: child_init
|
||||
responsibilities:
|
||||
- Initialize module state for each Apache child process.
|
||||
- Reset per-process socket state (fd, timers, error flags).
|
||||
- Attempt initial non-blocking connection to Unix socket if configured.
|
||||
- name: child_exit
|
||||
responsibilities:
|
||||
- Cleanly close Unix socket file descriptor if open.
|
||||
- name: post_read_request
|
||||
responsibilities:
|
||||
- Retrieve per-process server configuration (thread-safe).
|
||||
- Ensure Unix socket is connected (with periodic reconnect).
|
||||
- Build JSON log document for the request.
|
||||
- Write JSON line to Unix socket using non-blocking I/O.
|
||||
- Handle errors by dropping the current log line and rate-limiting
|
||||
error reports into Apache error_log.
|
||||
thread_safety:
|
||||
model: per-process-state
|
||||
description: >
|
||||
Each Apache child process maintains its own socket state stored in the
|
||||
server configuration structure (reqin_log_server_conf_t). This avoids
|
||||
race conditions in worker and event MPMs where multiple threads share
|
||||
a process.
|
||||
implementation:
|
||||
- State stored via ap_get_module_config(s->module_config)
|
||||
- No global variables for socket state
|
||||
- Each process has independent: socket_fd, connect timers, error timers
|
||||
data_model:
|
||||
json_line:
|
||||
description: >
|
||||
One JSON object per HTTP request, serialized on a single line and
|
||||
terminated by "\n".
|
||||
terminated by "\n". Uses flat structure with header fields at root level.
|
||||
structure: flat
|
||||
fields:
|
||||
- name: time
|
||||
type: string
|
||||
@ -69,8 +81,9 @@ module:
|
||||
type: integer
|
||||
unit: nanoseconds
|
||||
description: >
|
||||
Monotonic or wall-clock based timestamp in nanoseconds since an
|
||||
implementation-defined epoch (stable enough for ordering and latency analysis).
|
||||
Wall-clock timestamp in nanoseconds since Unix epoch.
|
||||
Note: apr_time_now() returns microseconds, multiplied by 1000 for nanoseconds.
|
||||
example: 1708948770000000000
|
||||
- name: src_ip
|
||||
type: string
|
||||
example: "192.0.2.10"
|
||||
@ -95,15 +108,30 @@ module:
|
||||
- name: http_version
|
||||
type: string
|
||||
example: "HTTP/1.1"
|
||||
- name: headers
|
||||
type: object
|
||||
- name: header_<HeaderName>
|
||||
type: string
|
||||
description: >
|
||||
Flattened headers from the configured header list. Keys are derived
|
||||
from configured header names prefixed with 'header_'.
|
||||
Flattened header fields at root level. For each configured header <H>,
|
||||
a field 'header_<H>' is added directly to the JSON root object.
|
||||
Headers are only included if present in the request.
|
||||
key_pattern: "header_<configured_header_name>"
|
||||
optional: true
|
||||
example:
|
||||
header_X-Request-Id: "abcd-1234"
|
||||
header_User-Agent: "curl/7.70.0"
|
||||
example_full:
|
||||
time: "2026-02-26T11:59:30Z"
|
||||
timestamp: 1708948770000000000
|
||||
src_ip: "192.0.2.10"
|
||||
src_port: 45678
|
||||
dst_ip: "198.51.100.5"
|
||||
dst_port: 443
|
||||
method: "GET"
|
||||
path: "/api/users"
|
||||
host: "example.com"
|
||||
http_version: "HTTP/1.1"
|
||||
header_X-Request-Id: "abcd-1234"
|
||||
header_User-Agent: "curl/7.70.0"
|
||||
|
||||
configuration:
|
||||
scope: global
|
||||
@ -258,44 +286,75 @@ constraints:
|
||||
testing:
|
||||
strategy:
|
||||
unit_tests:
|
||||
framework: cmocka
|
||||
location: tests/unit/test_module_real.c
|
||||
focus:
|
||||
- JSON serialization with header truncation and header count limits.
|
||||
- Directive parsing and configuration merging (global scope).
|
||||
- Error-handling branches for non-blocking write and reconnect logic.
|
||||
- Dynamic buffer operations (dynbuf_t) with resize handling.
|
||||
- ISO8601 timestamp formatting.
|
||||
- Header value truncation to JsonSockLogMaxHeaderValueLen.
|
||||
- Control character escaping in JSON strings.
|
||||
execution:
|
||||
- docker build -f Dockerfile.tests .
|
||||
- docker run --rm <image> ctest --output-on-failure
|
||||
integration_tests:
|
||||
framework: python3
|
||||
location: tests/integration/test_integration.py
|
||||
env:
|
||||
server: apache-httpd 2.4
|
||||
os: rocky-linux-8+
|
||||
log_consumer: simple Unix socket server capturing JSON lines
|
||||
log_consumer: Unix socket server (Python threading)
|
||||
scenarios:
|
||||
- name: basic_logging
|
||||
description: >
|
||||
With JsonSockLogEnabled On and valid socket, verify that each request
|
||||
produces a valid JSON line with expected fields.
|
||||
produces a valid JSON line with all required fields.
|
||||
checks:
|
||||
- All required fields present (time, timestamp, src_ip, dst_ip, method, path, host)
|
||||
- Field types correct (timestamp is integer, time is ISO8601 string)
|
||||
- Method matches HTTP request method
|
||||
- name: header_limits
|
||||
description: >
|
||||
Configure more headers than JsonSockLogMaxHeaders and verify only
|
||||
the first N are logged and values are truncated according to
|
||||
JsonSockLogMaxHeaderValueLen.
|
||||
the first N are logged and values are truncated.
|
||||
checks:
|
||||
- Header values truncated to JsonSockLogMaxHeaderValueLen (default: 256)
|
||||
- Only configured headers appear in output
|
||||
- name: socket_unavailable_on_start
|
||||
description: >
|
||||
Start Apache with JsonSockLogEnabled On but socket not yet created;
|
||||
verify periodic reconnect attempts and throttled error logging.
|
||||
checks:
|
||||
- Requests succeed even when socket unavailable
|
||||
- Module reconnects when socket becomes available
|
||||
- name: runtime_socket_loss
|
||||
description: >
|
||||
Drop the Unix socket while traffic is ongoing; verify that log lines
|
||||
are dropped, worker threads are not blocked, and reconnect attempts
|
||||
resume once the socket reappears.
|
||||
checks:
|
||||
- Requests complete quickly (<2s) when socket is down
|
||||
- Module recovers and logs again after socket restoration
|
||||
execution:
|
||||
- python3 tests/integration/test_integration.py --url http://localhost:8080
|
||||
bash_tests:
|
||||
location: scripts/run_integration_tests.sh
|
||||
description: >
|
||||
Legacy bash-based integration tests for simple validation.
|
||||
Tests JSON format, required fields, header logging via curl and grep.
|
||||
execution:
|
||||
- bash scripts/run_integration_tests.sh
|
||||
|
||||
|
||||
ci:
|
||||
strategy:
|
||||
description: >
|
||||
All builds, tests and packaging are executed inside Docker containers.
|
||||
The host only needs Docker and the CI runner.
|
||||
The host only needs Docker and the CI runner (GitHub Actions).
|
||||
tools:
|
||||
orchestrator: "to-define (GitLab CI / GitHub Actions / autre)"
|
||||
orchestrator: GitHub Actions
|
||||
container_engine: docker
|
||||
workflow_file: .github/workflows/ci.yml
|
||||
stages:
|
||||
- name: build
|
||||
description: >
|
||||
@ -305,6 +364,7 @@ ci:
|
||||
- name: build-rocky-8
|
||||
image: "rockylinux:8"
|
||||
steps:
|
||||
- checkout: actions/checkout@v4
|
||||
- install_deps:
|
||||
- gcc
|
||||
- make
|
||||
@ -314,41 +374,58 @@ ci:
|
||||
- apr-util-devel
|
||||
- rpm-build
|
||||
- build_module:
|
||||
command: "apxs -c -i src/mod_reqin_log.c"
|
||||
command: "make APXS=/usr/bin/apxs"
|
||||
- verify:
|
||||
command: "ls -la modules/mod_reqin_log.so"
|
||||
- upload_artifact: actions/upload-artifact@v4
|
||||
- name: build-debian
|
||||
image: "debian:stable"
|
||||
steps:
|
||||
- checkout: actions/checkout@v4
|
||||
- install_deps:
|
||||
- build-essential
|
||||
- apache2
|
||||
- apache2-dev
|
||||
- debhelper
|
||||
- build_module:
|
||||
command: "apxs -c -i src/mod_reqin_log.c"
|
||||
command: "make APXS=/usr/bin/apxs"
|
||||
- verify:
|
||||
command: "ls -la modules/mod_reqin_log.so"
|
||||
- upload_artifact: actions/upload-artifact@v4
|
||||
|
||||
- name: test
|
||||
description: >
|
||||
Run unit tests (C) and integration tests (Apache + Unix socket consumer)
|
||||
inside Docker containers.
|
||||
Run unit tests (C with cmocka) inside Docker containers.
|
||||
Integration tests require a running Apache instance.
|
||||
jobs:
|
||||
- name: unit-tests
|
||||
image: "debian:stable"
|
||||
steps:
|
||||
- install_test_deps:
|
||||
- build-essential
|
||||
- cmake
|
||||
- "test-framework (à définir: cmocka, criterion, ...)"
|
||||
- run_tests:
|
||||
command: "ctest || make test"
|
||||
- name: integration-tests-rocky-8
|
||||
image: "rockylinux:8"
|
||||
steps:
|
||||
- prepare_apache_and_module
|
||||
- start_unix_socket_consumer
|
||||
- run_http_scenarios:
|
||||
description: >
|
||||
Validate JSON logs, header limits, socket loss and reconnect
|
||||
behaviour using curl/ab/siege or similar tools.
|
||||
- checkout: actions/checkout@v4
|
||||
- install_deps:
|
||||
- gcc
|
||||
- make
|
||||
- httpd
|
||||
- httpd-devel
|
||||
- apr-devel
|
||||
- apr-util-devel
|
||||
- cmake
|
||||
- git
|
||||
- pkgconfig
|
||||
- libxml2-devel
|
||||
- build_cmocka:
|
||||
description: "Build cmocka from source (not available in EPEL)"
|
||||
command: |
|
||||
cd /tmp && git clone https://git.cryptomilk.org/projects/cmocka.git
|
||||
cd cmocka && git checkout cmocka-1.1.5
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release
|
||||
make && make install && ldconfig
|
||||
- build_tests:
|
||||
command: |
|
||||
mkdir -p build/tests
|
||||
cd build/tests && cmake ../../ && make
|
||||
- run_tests:
|
||||
command: "cd build/tests && ctest --output-on-failure"
|
||||
|
||||
- name: package
|
||||
description: >
|
||||
@ -360,37 +437,48 @@ ci:
|
||||
- install_deps:
|
||||
- rpm-build
|
||||
- rpmlint
|
||||
- "build deps same as 'build-rocky-8'"
|
||||
- gcc
|
||||
- make
|
||||
- httpd
|
||||
- httpd-devel
|
||||
- create_tarball:
|
||||
command: "tar -czf mod_reqin_log-1.0.0.tar.gz --transform 's,^,mod_reqin_log-1.0.0/,' ."
|
||||
- setup_rpmbuild:
|
||||
command: |
|
||||
mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
cp mod_reqin_log-1.0.0.tar.gz ~/rpmbuild/SOURCES/
|
||||
cp packaging/rpm/mod_reqin_log.spec ~/rpmbuild/SPECS/
|
||||
- build_rpm:
|
||||
spec_file: "packaging/rpm/mod_reqin_log.spec"
|
||||
command: "rpmbuild -ba packaging/rpm/mod_reqin_log.spec"
|
||||
- artifacts:
|
||||
paths:
|
||||
- "dist/rpm/**/*.rpm"
|
||||
command: "rpmbuild -ba ~/rpmbuild/SPECS/mod_reqin_log.spec"
|
||||
- upload_artifact:
|
||||
paths: "~/rpmbuild/RPMS/x86_64/*.rpm"
|
||||
- name: deb-debian
|
||||
image: "debian:stable"
|
||||
steps:
|
||||
- install_deps:
|
||||
- devscripts
|
||||
- build-essential
|
||||
- apache2
|
||||
- apache2-dev
|
||||
- debhelper
|
||||
- devscripts
|
||||
- dpkg-dev
|
||||
- "build deps same as 'build-debian'"
|
||||
- build_deb:
|
||||
- setup_package:
|
||||
command: |
|
||||
cd packaging/deb
|
||||
debuild -us -uc
|
||||
- artifacts:
|
||||
paths:
|
||||
- "dist/deb/**/*.deb"
|
||||
cp -r packaging/deb/* ./debian/
|
||||
# Create changelog
|
||||
- build_deb:
|
||||
command: "debuild -us -uc -b"
|
||||
- upload_artifact:
|
||||
paths: "../*.deb"
|
||||
|
||||
artifacts:
|
||||
retention:
|
||||
policy: "keep build logs and packages long enough for debugging (to define)"
|
||||
policy: "Keep build logs and packages for 30 days for debugging"
|
||||
outputs:
|
||||
- type: module
|
||||
path: "dist/modules/mod_reqin_log.so"
|
||||
path: "modules/mod_reqin_log.so"
|
||||
- type: rpm
|
||||
path: "dist/rpm/"
|
||||
path: "~/rpmbuild/RPMS/x86_64/"
|
||||
- type: deb
|
||||
path: "dist/deb/"
|
||||
path: "../"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user