feat(logging): Implement database logging with app_log table

- Created migration to set up app_log table for storing application logs.
- Added AppLog entity and repository for ORM handling of logs.
- Developed DbLogger service to persist logs of level WARNING and above to the database while maintaining existing logging behavior.
- Implemented tests for admin log retrieval and DbLogger functionality to ensure proper logging behavior.
- Enhanced logging context sanitization for better error tracking.
This commit is contained in:
hamed
2026-06-29 20:01:03 +03:30
parent 830f7e8d0c
commit 803196108c
38 changed files with 3284 additions and 618 deletions
@@ -18,6 +18,7 @@ use App\Representation\Entity\Representation;
use App\Secretary\Entity\DoctorSecretary;
use App\Settlement\Entity\FinancialBreakdown;
use App\Settlement\Entity\Settlement;
use App\Shared\Logging\AppLog;
use App\Sms\Entity\SmsLog;
use App\Sms\Entity\SmsTemplate;
use App\Shared\Controller\BaseController;
@@ -1929,4 +1930,63 @@ class AdminApiController extends BaseController
'period' => ['from' => $from, 'to' => $to],
]);
}
// ── Application logs ────────────────────────────────────────────────────────
#[OA\Get(
path: '/api/v1/admin/logs',
summary: 'List persisted application logs (paginated)',
security: [['bearerAuth' => []]],
parameters: [
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 25)),
new OA\Parameter(name: 'level', in: 'query', required: false, description: 'PSR level filter (warning/error/critical/...)', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'search', in: 'query', required: false, description: 'substring match on message', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'from', in: 'query', required: false, description: 'unix timestamp lower bound', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'to', in: 'query', required: false, description: 'unix timestamp upper bound', schema: new OA\Schema(type: 'integer')),
],
responses: [new OA\Response(response: 200, description: 'Paginated list of logs')]
)]
#[Route('/api/v1/admin/logs', methods: ['GET'])]
public function logs(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(5, (int) $request->query->get('limit', 25)));
$level = trim((string) $request->query->get('level', ''));
$search = trim((string) $request->query->get('search', ''));
$from = trim((string) $request->query->get('from', ''));
$to = trim((string) $request->query->get('to', ''));
$qb = $this->em->createQueryBuilder()
->select('l.id, l.level, l.message, l.context, l.channel, l.path, l.createdAt')
->from(AppLog::class, 'l');
if ($level !== '') {
$qb->andWhere('l.level = :level')->setParameter('level', $level);
}
if ($search !== '') {
$qb->andWhere('l.message LIKE :s')->setParameter('s', '%' . $search . '%');
}
if ($from !== '') {
$qb->andWhere('l.createdAt >= :from')->setParameter('from', (int) $from);
}
if ($to !== '') {
$qb->andWhere('l.createdAt <= :to')->setParameter('to', (int) $to);
}
$qb->orderBy('l.id', 'DESC');
$total = (clone $qb)->select('COUNT(l.id)')->getQuery()->getSingleScalarResult();
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)->getQuery()->getArrayResult();
return $this->paginated(array_map(fn(array $l) => [
'id' => (int) $l['id'],
'level' => $l['level'],
'message' => $l['message'],
'context' => $l['context'],
'channel' => $l['channel'],
'path' => $l['path'],
'created_at' => (int) $l['createdAt'],
], $rows), (int) $total, $page, $limit);
}
}