AI-Enhanced Log Analysis and Anomaly Alert System — Part 6: Creating a PHP Dashboard for Alerts and Visualizations
body {font-family: Arial, sans-serif; line-height: 1.6; margin: 2rem; color:#333;}
h2 {color:#2c3e50; margin-top:2rem;}
h3 {color:#34495e; margin-top:1.5rem;}
table {border-collapse:collapse; width:100%; margin:1rem 0;}
th, td {border:1px solid #ddd; padding:0.5rem; text-align:left;}
th {background:#f4f4f4;}
pre {background:#f9f9f9; padding:1rem; overflow:auto; border:1px solid #e1e1e1;}
code {font-family: Consolas, monospace; color:#c7254e;}
.note {background:#fff8e1; border-left:4px solid #ffeb3b; padding:0.5rem 1rem; margin:1rem 0;}
AI-Enhanced Log Analysis and Anomaly Alert System — Part 6: Creating a PHP Dashboard for Alerts and Visualizations
Based on my technical understanding as a Lead Programmer Analyst (PHP, Perl, Python, Shell), I’ll walk you through building a production‑ready, single‑page PHP dashboard that surfaces AI‑generated anomalies, lets ops engineers triage them, and visualizes trends with modern JavaScript charts. This article is the sixth installment of the “AI‑Enhanced Log Analysis and Anomaly Alert System” series.
Quick Recap of Parts 1‑5
In Part 1 we defined the log ingestion pipeline (Fluent Bit → OpenObserve). Part 2 showed how to train a Claude 4.1‑based “reasoning agent” that turns raw logs into structured anomaly events. Part 3 covered the REST API that exposes those events, Part 4 demonstrated how to store alerts in PostgreSQL, and Part 5 introduced a lightweight Python service that periodically re‑scores logs using GPT‑5 parallel agents for early‑failure prediction.
Why a PHP Dashboard?
PHP remains the lingua franca for many internal tools, especially when you need tight integration with existing authentication layers (LDAP, SSO) and a quick time‑to‑market. In 2026, the market still favors purpose‑built log visualizers such as OpenObserve, Grafana, and Kibana, but they fall short on conversational investigation and real‑time AI feedback. As noted in OpenObserve’s “Best Log Visualization Tools in 2026”, Kibana’s AI features require the Elastic ML add‑on and still lack a natural‑language interface. Our PHP dashboard fills that gap by embedding the AI agent directly into the UI, letting users ask “Why did this spike happen?” and get a synthesized answer on the spot.
Architecture Overview
ComponentTechnologyResponsibility
Log IngestionFluent Bit → OpenObserveCollect raw logs, forward to storage.
AI Reasoning ServiceClaude 4.1 + GPT‑5 parallel agents (Python)Detect anomalies, generate explanations.
Alert APIFastAPI (Python) – `/api/alerts`Expose JSON payloads for the dashboard.
PersistencePostgreSQL (alerts, profiles)Store anomaly metadata, user acknowledgments.
DashboardPHP 8.2 (Laravel‑lite) + Vue 3 + Chart.jsRender tables, charts, and conversational UI.
Prerequisites
- PHP 8.2+ with `pdo_pgsql` extension enabled.
- Composer (for autoloading only – we’ll keep the stack minimal).
- Node.js (optional, only if you want to compile Vue components; we’ll use CDN‑based builds).
- Access to the `/api/alerts` endpoint from Part 3 (CORS enabled for `https://your‑dashboard.domain`).
- A PostgreSQL database with the `alerts` table created in Part 4.
Step 1 – Bootstrap the PHP Project
Start by creating a folder called dashboard and initialise Composer. We’ll use PSR‑4 autoloading for a tiny “service” layer that talks to the Alert API.
mkdir dashboard && cd dashboard
composer init --name="yourorg/ai‑log‑dashboard" --require="guzzlehttp/guzzle:^7.8" --no-interaction
composer install
Now create the directory structure:
dashboard/
├─ public/
│ └─ index.php
├─ src/
│ ├─ ApiClient.php
│ └─ AlertRepository.php
├─ vendor/
└─ composer.json
Step 2 – API Client (src/ApiClient.php)
The client uses Guzzle to fetch alerts. It also adds a tiny retry logic that’s useful when the AI service is temporarily throttling.
<?php
namespace Dashboard;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
class ApiClient
{
private Client $client;
private string $baseUri;
public function __construct(string $baseUri, int $timeout = 5)
{
$this->baseUri = rtrim($baseUri, '/');
$this->client = new Client([
'base_uri' => $this->baseUri,
'timeout' => $timeout,
'http_errors' => false,
'verify' => false // In prod use proper certs
]);
}
/**
* Fetch alerts with optional query parameters.
*
* @param array $query ['severity' => 'high', 'since' => '2026-09-01']
* @return array
*/
public function fetchAlerts(array $query = []): array
{
$uri = '/api/alerts';
$attempts = 3;
while ($attempts--) {
try {
$response = $this->client->get($uri, ['query' => $query]);
$code = $response->getStatusCode();
if ($code === 200) {
return json_decode($response->getBody(), true);
}
// Non‑200 – log and retry after a short sleep
error_log("Alert API returned $code, retrying…");
sleep(1);
} catch (TransferException $e) {
error_log('Alert API network error: '.$e->getMessage());
sleep(1);
}
}
// If we reach here, return empty list – dashboard will show a friendly message.
return [];
}
}
?>
Step 3 – Repository Layer (src/AlertRepository.php)
The repository abstracts pagination, filtering, and optional enrichment (e.g., fetching a related “root cause” from the AI agent on demand).
<?php
namespace Dashboard;
class AlertRepository
{
private ApiClient $api;
public function __construct(ApiClient $api)
{
$this->api = $api;
}
/**
* Returns a paginated slice of alerts.
*
* @param int $page 1‑based page number
* @param int $limit items per page
* @param array $filters ['severity'=>'high','service'=>'auth']
* @return array ['data'=>[], 'total'=>int]
*/
public function getPaginated(int $page = 1, int $limit = 20, array $filters = []): array
{
$offset = ($page - 1) * $limit;
$query = array_merge($filters, [
'limit' => $limit,
'offset' => $offset,
]);
$payload = $this->api->fetchAlerts($query);
// The AI service already returns total count; if not, we estimate.
$total = $payload['total'] ?? ($offset + count($payload['alerts'] ?? []));
return [
'data' => $payload['alerts'] ?? [],
'total' => $total,
];
}
}
?>
Step 4 – Front‑End Skeleton (public/index.php)
We’ll use Vue 3 via CDN for reactivity, Chart.js for visualizations, and DataTables (also CDN) for the tabular view. The page loads the first page of alerts via an AJAX call to backend.php (a thin PHP wrapper around the repository).
<?php
require DIR . '/../vendor/autoload.php';
use Dashboard\ApiClient;
use Dashboard\AlertRepository;
// Configuration – in real life pull from env vars
$apiBase = 'https://ai‑alert‑service.example.com';
$apiClient = new ApiClient($apiBase);
$repo = new AlertRepository($apiClient);
// Simple router – only /backend.php is used by the front‑end
if ($_SERVER['REQUEST_METHOD'] === 'GET' && strpos($_SERVER['REQUEST_URI'], '/backend.php') === 0) {
$page = (int)($_GET['page'] ?? 1);
$limit = (int)($_GET['limit'] ?? 20);
$sev = $_GET['severity'] ?? null;
$service = $_GET['service'] ?? null;
$filters = array_filter([
'severity' => $sev,
'service' => $service,
]);
$result = $repo->getPaginated($page, $limit, $filters);
header('Content-Type: application/json');
echo json_encode($result);
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AI‑Log Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.0/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.datatables.net/2.0.8/js/dataTables.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.8/css/dataTables.bootstrap5.min.css">
<style>
.severity-high {color:#c0392b; font-weight:bold;}
.severity-medium {color:#e67e22;}
.severity-low {color:#27ae60;}
.chart-container {position:relative; height:300px;}
</style>
</head>
<body class="bg-light">
<div id="app" class="container py-4">
<h2 class="mb-4">AI‑Enhanced Log Anomaly Dashboard</h2>
<!-- Filter Bar -->
<div class="row g-3 mb-3">
<div class="col-md-3">
<select v-model="filters.severity" class="form-select">
<option value="">All Severities</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div class="col-md-3">
<input v-model="filters.service" type="text" class="form-control" placeholder="Service name">
</div>
<div class="col-md-2">
<button @click="loadAlerts" class="btn btn-primary w-100">Apply</button>
</div>
</div>
<!-- Charts Row -->
<div class="row g-4">
<div class="col-lg-6">
<div class="card">
<div class="card-header">Alerts Over Time</div>
<div class="card-body">
<canvas id="timeChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header">Severity Distribution</div>
<div class="card-body">
<canvas id="severityChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Alerts Table -->
<div class="mt-4">
<table id="alertsTable" class="table table-hover table-striped">
<thead class="table-dark">
<tr>
<th>Time</th>
<th>Service</th>
<th>Severity</th>
<th>Message</th>
<th>Root‑Cause (AI)</th>
</tr>
</thead>
<tbody>
<tr v-for="alert in alerts" :key="alert.id">
<td>{{ alert.timestamp }}</td>
<td>{{ alert.service }}</td>
<td :class="'severity-'+alert.severity">{{ alert.severity | capitalize }}</td>
<td>{{ alert.message }}</td>
<td>
<button @click="fetchRootCause(alert)" class="btn btn-sm btn-outline-info">
{{ alert.root_cause ? 'View' : 'Explain' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mt-3">
<li class="page-item" :class="{disabled: page===1}">
<a class="page-link" href="#" @click.prevent="changePage(page-1)">Previous</a>
</li>
<li class="page-item" :class="{active: p===page}" v-for="p in totalPages" :key="p">
<a class="page-link" href="#" @click.prevent="changePage(p)">{{ p }}</a>
</li>
<li class="page-item" :class="{disabled: page===totalPages}">
<a class="page-link" href="#" @click.prevent="changePage(page+1)">Next</a>
</li>
</ul>
</nav>
</div>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
alerts: [],
page: 1,
limit: 20,
total: 0,
filters: {
severity: '',
service: ''
},
timeChart: null,
severityChart: null,
};
},
computed: {
totalPages() {
return Math.ceil(this.total / this.limit) || 1;
}
},
methods: {
loadAlerts() {
const params = {
page: this.page,
limit: this.limit,
severity: this.filters.severity,
service: this.filters.service
};
axios.get('backend.php', { params })
.then(res => {
this.alerts = res.data.data;
this.total = res.data.total;
this.renderCharts();
// Re‑initialize DataTables after Vue updates the DOM
this.$nextTick(() => {
if ( $.fn.DataTable.isDataTable('#alertsTable') ) {
$('#alertsTable').DataTable().destroy();
}
$('#alertsTable').DataTable({
paging: false,
info: false,
searching: false
});
});
})
.catch(err => console.error(err));
},
change
Originally published at https://artificial-inteligence.phptutorial.co.in
Top comments (0)