mrnaeem4 / ci3-request-analysis
CodeIgniter 3 hook that intercepts incoming HTTP requests and writes structured JSON request logs (JSONL) to the CI3 logs directory with daily rotation and gzip compression.
Requires
- php: >=7.4
README
CodeIgniter 3 hook that intercepts incoming HTTP requests, extracts structured
metadata (headers, body, files, tenant, source IP, etc.), and writes one JSON
Line per request directly to the CI3 application/logs directory — with
sensitive-field redaction, body truncation, IP whitelisting, and daily log
rotation with gzip compression.
Local logging only. No external server, no queue, no Guzzle. This is the CI3 counterpart of ci4-request-analysis.
Features
- Attachable per-request via a single CI3 hook (
post_controller_constructor), not a global middleware. - Writes directly to
application/logs/analysis.logas JSON Lines (JSONL). - Daily rotation: a file from a previous day is renamed and gzip-compressed automatically on the next write.
- Retention pruning: compressed logs older than
REQUEST_LOG_RETENTION_DAYS(default 30) are deleted. - Configurable sensitive-field redaction (default:
password,nik,Api-Key,no_telp). - Raw body truncation at 3 MB (configurable) with
... [truncated]suffix. - File upload metadata captured without binary content (name, size, MIME, extension, SHA-256 hash, double-extension detection).
- IP/CIDR whitelist to skip private/internal traffic.
- PHP 7.4+.
Requirements
- PHP 7.4+
- CodeIgniter 3.x
- Composer (for autoloading), with a writable
application/logsdirectory
Installation
composer require mrnaeem4/ci3-request-analysis
Custom
vendor-dir: If yourcomposer.jsonsets"vendor-dir"(e.g.application/third_party/vendor), the package installs there. That is fine — just point$config['composer_autoload']at that exactvendor/autoload.phppath. CodeIgniter'sTRUEshortcut only looks atapplication/vendor/and the project root.
If you do not use Composer, add a PSR-4 autoloader mapping
MrNaeem\Ci3RequestAnalysis\ to the src/ directory, or require the three
classes manually:
require_once __DIR__ . '/vendor/mrnaeem4/ci3-request-analysis/src/Config/RequestLog.php'; require_once __DIR__ . '/vendor/mrnaeem4/ci3-request-analysis/src/Services/RequestLogService.php'; require_once __DIR__ . '/vendor/mrnaeem4/ci3-request-analysis/src/Hooks/RequestLogHook.php';
Configuration
1. Enable hooks + Composer autoload
In application/config/config.php:
$config['enable_hooks'] = TRUE; $config['composer_autoload'] = TRUE; // or absolute path to vendor/autoload.php
2. Register the hook
Important: CI3's array hook format (
class/function/filename/filepath) cannot resolve Composer namespaced classes —CI_Hooks::_run_hook()checksclass_exists($class, false)(no autoload) and thenrequire_onces thefilepath/filename, which fails for vendor files. Use a closure instead (CI3 supports callables natively):
In application/config/hooks.php:
$hook['post_controller_constructor'] = function () { (new \MrNaeem\Ci3RequestAnalysis\Hooks\RequestLogHook())->before(); };
3. Load the .env file
CI3 does not parse a .env file by itself, and the config values are read
with getenv(). Without loading the file, REQUEST_LOG_ENABLED is empty and
the hook silently does nothing. Pick one:
Option A — vlucas/phpdotenv (recommended):
composer require vlucas/phpdotenv
Load it in index.php (the front controller) before requiring
CodeIgniter.php:
require_once FCPATH . 'vendor/autoload.php'; $dotenv = Dotenv\Dotenv::createUnsafeImmutable(FCPATH); $dotenv->safeLoad();
Option B — Apache SetEnv:
# .htaccess SetEnv REQUEST_LOG_ENABLED true SetEnv REQUEST_LOG_REDACT_FIELDS "password,nik,Cookie,Api-Key,no_telp"
Option C — Nginx fastcgi_param:
location ~ \.php$ { fastcgi_param REQUEST_LOG_ENABLED true; # ... }
4. Set environment variables
Values of the .env file (or server env):
REQUEST_LOG_ENABLED = true REQUEST_LOG_DIR = "" # empty → application/logs REQUEST_LOG_FILE = "analysis.log" REQUEST_LOG_REDACT_FIELDS = "password,nik,Api-Key,no_telp" REQUEST_LOG_MAX_BODY_SIZE = 3145728 REQUEST_LOG_WHITELIST_IPS = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1" REQUEST_LOG_TRUNCATE_SUFFIX = "... [truncated]" REQUEST_LOG_RETENTION_DAYS = 30 REQUEST_LOG_METHODS = "" # empty = log all; e.g. "POST,PUT,DELETE" REQUEST_LOG_INCLUDE_PATHS = "" # empty = log all; regex, e.g. "^/api/" REQUEST_LOG_EXCLUDE_PATHS = "" # regex, e.g. "^/assets/,^/health$"
Config reference
| Variable | Default | Description |
|---|---|---|
REQUEST_LOG_ENABLED |
false |
Master switch for the hook. |
REQUEST_LOG_DIR |
'' (→ application/logs) |
Directory for the log file. |
REQUEST_LOG_FILE |
analysis.log |
Log file name (single file, rotated daily). |
REQUEST_LOG_REDACT_FIELDS |
password,nik,Api-Key,no_telp |
Comma-separated sensitive fields (case-insensitive). |
REQUEST_LOG_MAX_BODY_SIZE |
3145728 (3 MB) |
raw_body truncation length (bytes). |
REQUEST_LOG_WHITELIST_IPS |
RFC1918 + localhost | CIDR ranges to skip. |
REQUEST_LOG_TRUNCATE_SUFFIX |
... [truncated] |
Appended when the body is truncated. |
REQUEST_LOG_RETENTION_DAYS |
30 |
Days of compressed logs kept before pruning. |
REQUEST_LOG_METHODS |
'' (all) |
Comma-separated HTTP methods to log (e.g. POST,PUT). |
REQUEST_LOG_INCLUDE_PATHS |
'' (all) |
Regex allow-list for request paths (comma-separated). |
REQUEST_LOG_EXCLUDE_PATHS |
'' (none) |
Regex deny-list for request paths (comma-separated). |
Path & method filtering
By default every request (including GET /) is logged. Restrict it:
# Only mutating requests, only under /api/, never static assets or health checks REQUEST_LOG_METHODS = "POST,PUT,PATCH,DELETE" REQUEST_LOG_INCLUDE_PATHS = "^/api/" REQUEST_LOG_EXCLUDE_PATHS = "^/api/assets/,^/api/health$"
Patterns are regex matched against the request path (e.g. /api/upload).
includePaths is evaluated first; a request must match at least one include
pattern when that list is non-empty. excludePaths wins afterwards.
Log payload
Each line in analysis.log is a JSON object (envelope + log_data):
{
"log_data": {
"timestamp": "2026-08-31T02:15:04+00:00",
"domain": "app.example.com",
"path": "/api/profile/update",
"method": "POST",
"srcip": "203.0.113.10",
"user_agent": "Mozilla/5.0 ...",
"query_string": "page=1",
"headers": { "Content-Type": "application/json", ... },
"raw_body": "{\"name\":\"User\",\"email\":\"user@example.com\",\"password\":\"***REDACTED***\"}",
"file_count": 1,
"file_names": ["shell.php.jpg"],
"file_metadata": [
{
"original_name": "shell.php.jpg",
"size": 20480,
"mime_type": "image/jpeg",
"extension": "jpg",
"hash": "3c98...",
"has_double_extension": true
}
]
},
"retry_count": 0,
"last_attempt": null,
"created_at": "2026-08-31T02:15:04+00:00"
}
How it works
Request → CI3 Hook (post_controller_constructor)
├─ enabled? ──no──► done
├─ IP whitelisted? ──yes──► done
├─ method/path filter passes? ──no──► done
├─ collect: headers, body, files, tenant, srcip...
│ ├─ redact sensitive fields (headers + body)
│ ├─ truncate body at max size
│ └─ extract file metadata (no binary)
├─ rotate if the active log is from a previous day (rename + gzip + prune)
└─ append one JSONL line to application/logs/analysis.log
The write is a single append with LOCK_EX, so it does not block the request
and is safe for concurrent PHP-FPM workers.
Security
CI3 places index.php in the same directory as application/, so by default
everything under application/ — including application/logs/analysis.log,
which contains request headers and bodies — is reachable directly from the
browser (http://host/application/logs/analysis.log). Apply at least one of
the layers below; using all three is recommended.
1. Keep application/ out of the web root (recommended)
Move the front controller so the public web root only contains index.php,
.htaccess and assets:
project/
├── application/ ← not web-accessible
├── system/
└── public/ ← document root
├── index.php
└── .htaccess
Then fix the paths in public/index.php (a copy of the original, with
$system_path/$application_folder updated to ../system and ../application)
and set $config['base_url'] accordingly in application/config/config.php.
2. Redirect the log directory outside the web root
Even with the default layout, point logs somewhere the browser cannot reach:
REQUEST_LOG_DIR = "/var/log/myapp/request-analysis"
The hook falls back to application/logs only when REQUEST_LOG_DIR is empty.
3. Deny access with the web server
Apache — ship an .htaccess inside the log directory (and the
application/ directory) with:
<IfModule mod_authz_core.c> Require all denied </IfModule> <IfModule !mod_authz_core.c> Order deny,allow Deny from all </IfModule>
The sample app already includes sample/application/logs/.htaccess and
sample/application/uploads/.htaccess. If AllowOverride is disabled, deny
the paths in the vhost instead:
<Directory "/path/to/app/application/logs"> Require all denied </Directory>
Nginx:
location ~ ^/(application|system)/ { deny all; } location ^~ /application/logs/ { deny all; }
Laravel-style check — as a last line of defense, application/logs/.htaccess
also rejects *.log / *.gz matches even if directory-level rules are ignored.
Notes
- By default every request is logged (including
GET /). UseREQUEST_LOG_METHODS/REQUEST_LOG_INCLUDE_PATHS/REQUEST_LOG_EXCLUDE_PATHSto narrow it down — see Path & method filtering above. - Binary file content is never stored; only metadata is captured.
- Redaction applies to both request headers (e.g.
Cookie) and the body. application/logsshould not be publicly accessible.
Sample app
A minimal CI3 application fragment lives under sample/:
sample/application/config/config.php— hooks + Composer autoload enabledsample/application/config/hooks.php— hook registrationsample/application/controllers/Home.php—post+uploaddemo endpointssample/application/views/home/index.php— test forms
Copy the relevant files into an existing CI3 application and start your server.
Changelog
See CHANGELOG.md.