#feature: add ip operation log & sql generator
This commit is contained in:
129
app/Http/Middleware/OperationLogMiddleware.php
Normal file
129
app/Http/Middleware/OperationLogMiddleware.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\IpUserMapping;
|
||||
use App\Models\OperationLog;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Throwable;
|
||||
|
||||
class OperationLogMiddleware
|
||||
{
|
||||
/**
|
||||
* @param Closure(Request): Response $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$startAt = microtime(true);
|
||||
|
||||
try {
|
||||
$response = $next($request);
|
||||
} catch (Throwable $exception) {
|
||||
$this->recordLog($request, null, $startAt, 500);
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->recordLog($request, $response, $startAt, $response->getStatusCode());
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function shouldLog(Request $request): bool
|
||||
{
|
||||
$methods = config('toolbox.operation_log.methods', []);
|
||||
if (!in_array($request->method(), $methods, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $request->is('api/*');
|
||||
}
|
||||
|
||||
private function recordLog(Request $request, ?Response $response, float $startAt, int $statusCode): void
|
||||
{
|
||||
if (!$this->shouldLog($request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$ipAddress = $request->ip() ?? '';
|
||||
$mapping = $ipAddress !== ''
|
||||
? IpUserMapping::query()->where('ip_address', $ipAddress)->first()
|
||||
: null;
|
||||
|
||||
OperationLog::query()->create([
|
||||
'ip_address' => $ipAddress,
|
||||
'user_label' => $mapping?->user_name,
|
||||
'method' => $request->method(),
|
||||
'path' => '/' . ltrim($request->path(), '/'),
|
||||
'route_name' => $request->route()?->getName(),
|
||||
'status_code' => $statusCode,
|
||||
'duration_ms' => (int) round((microtime(true) - $startAt) * 1000),
|
||||
'request_payload' => $this->buildPayload($request),
|
||||
'user_agent' => substr((string) $request->userAgent(), 0, 255),
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
Log::warning('Failed to record operation log.', [
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildPayload(Request $request): array
|
||||
{
|
||||
$payload = $request->all();
|
||||
if (empty($payload)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$redactKeys = array_map('strtolower', config('toolbox.operation_log.redact_keys', []));
|
||||
$payload = $this->redactPayload($payload, $redactKeys);
|
||||
|
||||
$encoded = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
if ($encoded === false) {
|
||||
return ['_unserializable' => true];
|
||||
}
|
||||
|
||||
$maxLength = (int) config('toolbox.operation_log.max_payload_length', 2000);
|
||||
if (strlen($encoded) <= $maxLength) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
return [
|
||||
'_truncated' => true,
|
||||
'preview' => substr($encoded, 0, $maxLength),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<int, string> $redactKeys
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function redactPayload(array $payload, array $redactKeys): array
|
||||
{
|
||||
$redacted = [];
|
||||
|
||||
foreach ($payload as $key => $value) {
|
||||
$lowerKey = strtolower((string) $key);
|
||||
if (in_array($lowerKey, $redactKeys, true)) {
|
||||
$redacted[$key] = '[redacted]';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$redacted[$key] = $this->redactPayload($value, $redactKeys);
|
||||
continue;
|
||||
}
|
||||
|
||||
$redacted[$key] = $value;
|
||||
}
|
||||
|
||||
return $redacted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user