Skip to content

Commit a78fb67

Browse files
feat(filtering): Add method/path filters, fix hook docs and .env guidance
1 parent 702540c commit a78fb67

5 files changed

Lines changed: 212 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2026-09-02
9+
10+
### Added
11+
12+
- Request filtering: `REQUEST_LOG_METHODS` (limit logged HTTP methods),
13+
`REQUEST_LOG_INCLUDE_PATHS` (regex allow-list) and
14+
`REQUEST_LOG_EXCLUDE_PATHS` (regex deny-list) so not every `GET /` ends up
15+
in the log.
16+
17+
### Fixed
18+
19+
- Documentation: the array hook format in `hooks.php` never worked for
20+
Composer namespaced classes (`CI_Hooks::_run_hook()` bypasses autoloaders
21+
and `require_once`s the configured file path). The README, hook docblock and
22+
sample now use the working **closure** registration format.
23+
- Documentation: added an explicit `.env` loading section — CI3 does not parse
24+
`.env` files, so `REQUEST_LOG_*` must be exposed via vlucas/phpdotenv,
25+
Apache `SetEnv`, or Nginx `fastcgi_param`.
26+
- Documentation: added note about custom Composer `vendor-dir`
27+
(e.g. `application/third_party/vendor`) and pointing
28+
`$config['composer_autoload']` at the exact autoload path.
29+
830
## [1.0.1] - 2026-09-02
931

1032
### Fixed
@@ -36,5 +58,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3658
- Sample CI3 application under `sample/` with a Home controller demonstrating
3759
JSON POST and multipart file upload endpoints.
3860

61+
[1.1.0]: https://github.com/mrnaeem4/ci3-request-analysis/releases/tag/v1.1.0
3962
[1.0.1]: https://github.com/mrnaeem4/ci3-request-analysis/releases/tag/v1.0.1
4063
[1.0.0]: https://github.com/mrnaeem4/ci3-request-analysis/releases/tag/v1.0.0

README.md

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ rotation with gzip compression.
3737
composer require mrnaeem4/ci3-request-analysis
3838
```
3939

40+
> **Custom `vendor-dir`:** If your `composer.json` sets `"vendor-dir"`
41+
> (e.g. `application/third_party/vendor`), the package installs there. That is
42+
> fine — just point `$config['composer_autoload']` at that exact
43+
> `vendor/autoload.php` path. CodeIgniter's `TRUE` shortcut only looks at
44+
> `application/vendor/` and the project root.
45+
4046
If you do not use Composer, add a PSR-4 autoloader mapping
4147
`MrNaeem\Ci3RequestAnalysis\` to the `src/` directory, or require the three
4248
classes manually:
@@ -60,21 +66,62 @@ $config['composer_autoload'] = TRUE; // or absolute path to vendor/autoload.php
6066

6167
### 2. Register the hook
6268

69+
> **Important:** CI3's array hook format (`class`/`function`/`filename`/
70+
> `filepath`) cannot resolve Composer namespaced classes — `CI_Hooks::_run_hook()`
71+
> checks `class_exists($class, false)` (no autoload) and then `require_once`s
72+
> the `filepath`/`filename`, which fails for vendor files. Use a **closure**
73+
> instead (CI3 supports callables natively):
74+
6375
In `application/config/hooks.php`:
6476

6577
```php
66-
$hook['post_controller_constructor'] = [
67-
'class' => 'MrNaeem\Ci3RequestAnalysis\Hooks\RequestLogHook',
68-
'function' => 'before',
69-
'filename' => '',
70-
'filepath' => '',
71-
'params' => [],
72-
];
78+
$hook['post_controller_constructor'] = function () {
79+
(new \MrNaeem\Ci3RequestAnalysis\Hooks\RequestLogHook())->before();
80+
};
81+
```
82+
83+
### 3. Load the `.env` file
84+
85+
CI3 does **not** parse a `.env` file by itself, and the config values are read
86+
with `getenv()`. Without loading the file, `REQUEST_LOG_ENABLED` is empty and
87+
the hook silently does nothing. Pick one:
88+
89+
**Option A — vlucas/phpdotenv (recommended):**
90+
91+
```bash
92+
composer require vlucas/phpdotenv
93+
```
94+
95+
Load it in `index.php` (the front controller) **before** requiring
96+
`CodeIgniter.php`:
97+
98+
```php
99+
require_once FCPATH . 'vendor/autoload.php';
100+
101+
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(FCPATH);
102+
$dotenv->safeLoad();
103+
```
104+
105+
**Option B — Apache `SetEnv`:**
106+
107+
```apache
108+
# .htaccess
109+
SetEnv REQUEST_LOG_ENABLED true
110+
SetEnv REQUEST_LOG_REDACT_FIELDS "password,nik,Cookie,Api-Key,no_telp"
111+
```
112+
113+
**Option C — Nginx `fastcgi_param`:**
114+
115+
```nginx
116+
location ~ \.php$ {
117+
fastcgi_param REQUEST_LOG_ENABLED true;
118+
# ...
119+
}
73120
```
74121

75-
### 3. Set environment variables
122+
### 4. Set environment variables
76123

77-
Set in your web server environment or system environment:
124+
Values of the `.env` file (or server env):
78125

79126
```ini
80127
REQUEST_LOG_ENABLED = true
@@ -85,6 +132,9 @@ REQUEST_LOG_MAX_BODY_SIZE = 3145728
85132
REQUEST_LOG_WHITELIST_IPS = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
86133
REQUEST_LOG_TRUNCATE_SUFFIX = "... [truncated]"
87134
REQUEST_LOG_RETENTION_DAYS = 30
135+
REQUEST_LOG_METHODS = "" # empty = log all; e.g. "POST,PUT,DELETE"
136+
REQUEST_LOG_INCLUDE_PATHS = "" # empty = log all; regex, e.g. "^/api/"
137+
REQUEST_LOG_EXCLUDE_PATHS = "" # regex, e.g. "^/assets/,^/health$"
88138
```
89139

90140
### Config reference
@@ -99,6 +149,24 @@ REQUEST_LOG_RETENTION_DAYS = 30
99149
| `REQUEST_LOG_WHITELIST_IPS` | RFC1918 + localhost | CIDR ranges to skip. |
100150
| `REQUEST_LOG_TRUNCATE_SUFFIX` | `... [truncated]` | Appended when the body is truncated. |
101151
| `REQUEST_LOG_RETENTION_DAYS` | `30` | Days of compressed logs kept before pruning. |
152+
| `REQUEST_LOG_METHODS` | `''` (all) | Comma-separated HTTP methods to log (e.g. `POST,PUT`). |
153+
| `REQUEST_LOG_INCLUDE_PATHS` | `''` (all) | Regex allow-list for request paths (comma-separated). |
154+
| `REQUEST_LOG_EXCLUDE_PATHS` | `''` (none) | Regex deny-list for request paths (comma-separated). |
155+
156+
### Path & method filtering
157+
158+
By default **every** request (including `GET /`) is logged. Restrict it:
159+
160+
```ini
161+
# Only mutating requests, only under /api/, never static assets or health checks
162+
REQUEST_LOG_METHODS = "POST,PUT,PATCH,DELETE"
163+
REQUEST_LOG_INCLUDE_PATHS = "^/api/"
164+
REQUEST_LOG_EXCLUDE_PATHS = "^/api/assets/,^/api/health$"
165+
```
166+
167+
Patterns are regex matched against the request path (e.g. `/api/upload`).
168+
`includePaths` is evaluated first; a request must match at least one include
169+
pattern when that list is non-empty. `excludePaths` wins afterwards.
102170

103171
## Log payload
104172

@@ -141,6 +209,7 @@ Each line in `analysis.log` is a JSON object (envelope + `log_data`):
141209
Request → CI3 Hook (post_controller_constructor)
142210
├─ enabled? ──no──► done
143211
├─ IP whitelisted? ──yes──► done
212+
├─ method/path filter passes? ──no──► done
144213
├─ collect: headers, body, files, tenant, srcip...
145214
│ ├─ redact sensitive fields (headers + body)
146215
│ ├─ truncate body at max size
@@ -229,10 +298,9 @@ also rejects `*.log` / `*.gz` matches even if directory-level rules are ignored.
229298

230299
## Notes
231300

232-
- The hook runs on every request once enabled. To log only specific
233-
controllers, guard inside your controller or move the hook logic into a
234-
base-controller `__construct()` call — see
235-
`sample/application/controllers/Home.php` for a self-contained demo.
301+
- By default every request is logged (including `GET /`). Use
302+
`REQUEST_LOG_METHODS` / `REQUEST_LOG_INCLUDE_PATHS` / `REQUEST_LOG_EXCLUDE_PATHS`
303+
to narrow it down — see *Path & method filtering* above.
236304
- Binary file content is never stored; only metadata is captured.
237305
- Redaction applies to both request headers (e.g. `Cookie`) and the body.
238306
- `application/logs` should not be publicly accessible.

sample/application/config/hooks.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,12 @@
55
| -------------------------------------------------------------------------
66
| Hooks
77
| -------------------------------------------------------------------------
8-
| This file lets you define "hooks" to extend CI without hacking the core
9-
| files. Set $config['enable_hooks'] = TRUE in config.php first.
8+
| Set $config['enable_hooks'] = TRUE in config.php first.
109
|
11-
| Attach the request analysis hook to post_controller_constructor so it runs
12-
| after the controller is instantiated (Composer autoload is already loaded).
10+
| Use a CLOSURE — CI3's array hook format (class/filename/filepath) cannot
11+
| resolve Composer namespaced classes. See README "Register the hook".
1312
*/
1413

15-
$hook['post_controller_constructor'] = [
16-
'class' => 'MrNaeem\Ci3RequestAnalysis\Hooks\RequestLogHook',
17-
'function' => 'before',
18-
'filename' => '',
19-
'filepath' => '',
20-
'params' => [],
21-
];
14+
$hook['post_controller_constructor'] = function () {
15+
(new \MrNaeem\Ci3RequestAnalysis\Hooks\RequestLogHook())->before();
16+
};

src/Config/RequestLog.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ class RequestLog
2020

2121
public $retentionDays = 30;
2222

23+
/**
24+
* HTTP methods to log (uppercase). Empty array = log all methods.
25+
*
26+
* @var array
27+
*/
28+
public $logMethods = [];
29+
30+
/**
31+
* Regex patterns (against the request path). When non-empty, only paths
32+
* matching at least one pattern are logged.
33+
*
34+
* @var array
35+
*/
36+
public $includePaths = [];
37+
38+
/**
39+
* Regex patterns (against the request path). Matching paths are never
40+
* logged. Applied after includePaths.
41+
*
42+
* @var array
43+
*/
44+
public $excludePaths = [];
45+
2346
public function __construct()
2447
{
2548
$this->enabled = filter_var($this->env('REQUEST_LOG_ENABLED', $this->enabled), FILTER_VALIDATE_BOOLEAN);
@@ -38,6 +61,71 @@ public function __construct()
3861
if ($whitelist !== '') {
3962
$this->whitelistIps = $this->parseCsv($whitelist);
4063
}
64+
65+
$methods = (string) $this->env('REQUEST_LOG_METHODS', '');
66+
if ($methods !== '') {
67+
$this->logMethods = array_map('strtoupper', $this->parseCsv($methods));
68+
}
69+
70+
$include = (string) $this->env('REQUEST_LOG_INCLUDE_PATHS', '');
71+
if ($include !== '') {
72+
$this->includePaths = $this->parseCsv($include);
73+
}
74+
75+
$exclude = (string) $this->env('REQUEST_LOG_EXCLUDE_PATHS', '');
76+
if ($exclude !== '') {
77+
$this->excludePaths = $this->parseCsv($exclude);
78+
}
79+
}
80+
81+
/**
82+
* Decide whether a request (method + path) passes the logging filters.
83+
*/
84+
public function shouldLog(string $method, string $path): bool
85+
{
86+
$method = strtoupper($method);
87+
88+
$methods = array_map('strtoupper', (array) $this->logMethods);
89+
if ($methods !== [] && ! in_array($method, $methods, true)) {
90+
return false;
91+
}
92+
93+
$include = (array) $this->includePaths;
94+
if ($include !== [] && ! $this->matchesAny($path, $include)) {
95+
return false;
96+
}
97+
98+
$exclude = (array) $this->excludePaths;
99+
if ($exclude !== [] && $this->matchesAny($path, $exclude)) {
100+
return false;
101+
}
102+
103+
return true;
104+
}
105+
106+
protected function matchesAny(string $path, array $patterns): bool
107+
{
108+
foreach ($patterns as $pattern) {
109+
$pattern = trim((string) $pattern);
110+
if ($pattern === '') {
111+
continue;
112+
}
113+
114+
$result = @preg_match('~' . str_replace('~', '\~', $pattern) . '~', $path);
115+
116+
if ($result === 1) {
117+
return true;
118+
}
119+
120+
if ($result === false) {
121+
// Invalid regex: fall back to a plain prefix match.
122+
if (strpos($path, $pattern) === 0) {
123+
return true;
124+
}
125+
}
126+
}
127+
128+
return false;
41129
}
42130

43131
protected function env(string $key, $default)

src/Hooks/RequestLogHook.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,16 @@
77

88
/**
99
* CI3 hook that captures request metadata and writes one JSONL line to the
10-
* CI3 logs directory. Attach via application/config/hooks.php:
10+
* CI3 logs directory. Attach via application/config/hooks.php using a closure
11+
* (CI3's array hook format cannot resolve Composer namespaced classes):
1112
*
12-
* $hook['post_controller_constructor'] = [
13-
* 'class' => 'MrNaeem\\Ci3RequestAnalysis\\Hooks\\RequestLogHook',
14-
* 'function' => 'before',
15-
* 'filename' => '',
16-
* 'filepath' => '',
17-
* 'params' => [],
18-
* ];
13+
* $hook['post_controller_constructor'] = function () {
14+
* (new \MrNaeem\Ci3RequestAnalysis\Hooks\RequestLogHook())->before();
15+
* };
1916
*
20-
* Requires $config['composer_autoload'] = TRUE in application/config/config.php
21-
* (or Composer loaded manually before the hook fires).
17+
* Requires $config['composer_autoload'] pointing to your vendor/autoload.php
18+
* in application/config/config.php. The request environment must expose the
19+
* REQUEST_LOG_* variables (e.g. via vlucas/phpdotenv or SetEnv in Apache).
2220
*/
2321
class RequestLogHook
2422
{
@@ -51,6 +49,12 @@ public function before()
5149
return;
5250
}
5351

52+
$uri = (string) ($CI->input->server('REQUEST_URI') ?? '/');
53+
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
54+
if (! $this->config->shouldLog(strtoupper((string) $CI->input->method()), $path)) {
55+
return;
56+
}
57+
5458
$data = $this->gather($CI, $ip);
5559
$log = $this->service->collect($data);
5660

0 commit comments

Comments
 (0)