GoAccess in Filament: A Zero-Dependency, Privacy-First Analytics Pipeline for Laravel
A deep dive into building a self-hosted web analytics dashboard inside Filament using GoAccess, Nginx log parsing, and secure local file serving without tracking cookies or GDPR banners.
The Liability of Third-Party Analytics
Modern web analytics are dominated by client-side tracking scripts. These scripts inject latency, require intrusive cookie consent banners, and leak user browsing habits to external ad networks. For developer portfolios, technical blogs, and personal projects, this overhead is counterproductive.
The alternative is server-side log analysis. Every request hitting a web server is already logged. Nginx records the URI, response code, byte size, user agent, and timestamp. Analyzing these logs yields exact traffic statistics without executing JavaScript on the client, storing tracking cookies, or compromising user privacy.
This article details how to build a zero-dependency, self-hosted analytics dashboard inside Filament using Nginx, GoAccess, and Laravel.
Pipeline Architecture
The analytics pipeline operates in four stages:
- Log Generation: Nginx runs in a Docker container on Fly.io, writing Combined-format logs to a persistent volume at
storage/logs/nginx-access.log. - Log Parsing: A scheduled Artisan command calls a bash script to run GoAccess. This tool parses the raw logs and generates a self-contained, interactive HTML dashboard.
- Secure Storage: The generated HTML file is saved to secure local storage at
storage/app/goaccess/report.html, keeping it out of the public directory. - Authenticated Serving: A custom Filament page renders the report inside an iframe, protected by Laravel's administrative authentication middleware.
+------------+ +-----------+ +-------------------+ +-----------------+
| Client | ----> | Nginx | ----> | nginx-access.log | ----> | GoAccess |
+------------+ +-----------+ +-------------------+ +-----------------+
| |
v v
[Fly Volume] [report.html (app)]
|
v
+------------+ +-----------+ +-----------------+
| Admin User | ----> | Filament | <-------------------------------- | Secure Route |
+------------+ +-----------+ +-----------------+
Step 1: Nginx Configuration and Client IP Resolution
When running applications behind a load balancer, the client IP address ($remote_addr) recorded in standard Nginx logs is often the private IP of the balancer rather than the actual user. To resolve this, Nginx must extract the real client IP from headers injected by the load balancer.
In Fly.io environments, the load balancer adds the Fly-Client-Ip header. If that header is empty, the pipeline falls back to the standard X-Forwarded-For header.
The mapping is defined in the Nginx configuration block (.fly/nginx/conf.d/access-log.conf):
map $http_fly_client_ip $forwarded_header_value {
default $http_fly_client_ip;
"" $http_x_forwarded_for;
}
log_format goaccess_combined '$forwarded_header_value - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
Next, the Nginx server configuration (.fly/nginx/sites-available/default) points to this log format and saves the logs to the Laravel storage directory:
access_log /var/www/html/storage/logs/nginx-access.log goaccess_combined;
access_log /dev/stdout fly;
This setup ensures that nginx-access.log contains combined-format logs with corrected client IPs, ready for GoAccess.
Step 2: The GoAccess Generation Script
GoAccess compiles the log file into a single, interactive HTML page. A shell script handles the invocation parameters, ensuring IP addresses are anonymized and common web crawlers are excluded to keep the data clean.
The script (scripts/goaccess-report.sh) handles execution:
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="${1:-storage/logs/nginx-access.log}"
REPORT_FILE="${2:-storage/app/goaccess/report.html}"
if ! command -v goaccess >/dev/null 2>&1; then
echo "goaccess not found. Install locally or use the container image."
exit 1
fi
if [ ! -s "$LOG_FILE" ]; then
echo "Access log missing or empty: $LOG_FILE"
exit 1
fi
mkdir -p "$(dirname "$REPORT_FILE")"
goaccess "$LOG_FILE" --log-format=COMBINED --ignore-crawlers --anonymize-ip --no-color -o "$REPORT_FILE"
The --anonymize-ip flag strips the last octet of IPv4 addresses and the last 80 bits of IPv6 addresses, protecting visitor privacy while keeping geographical data intact.
Step 3: Integrating with Laravel and Filament
To expose this report inside the Filament admin area, we write a dedicated service to check log existence, verify package availability, and run the shell script.
The generator class (App\Services\GoAccessReportGenerator):
namespace App\Services;
use Symfony\Component\Process\Process;
use Illuminate\Support\Facades\File;
class GoAccessReportGenerator
{
public function logPath(): string
{
return storage_path('logs/nginx-access.log');
}
public function reportPath(): string
{
return storage_path('app/goaccess/report.html');
}
public function generate(): void
{
if (!is_readable($this->logPath()) || filesize($this->logPath()) === 0) {
throw new \RuntimeException('Nginx access log is empty or missing.');
}
File::ensureDirectoryExists(dirname($this->reportPath()));
$process = new Process([
'bash',
base_path('scripts/goaccess-report.sh'),
$this->logPath(),
$this->reportPath()
], base_path());
$process->setTimeout(120);
$process->run();
if (!$process->isSuccessful()) {
throw new \RuntimeException($process->getErrorOutput() ?: 'GoAccess failed.');
}
}
}
An Artisan command (App\Console\Commands\GoAccessReportCommand) exposes this service to scheduled cron jobs:
namespace App\Console\Commands;
use App\Services\GoAccessReportGenerator;
use Illuminate\Console\Command;
class GoAccessReportCommand extends Command
{
protected $signature = 'analytics:goaccess';
protected $description = 'Build a GoAccess HTML report from nginx access logs';
public function handle(GoAccessReportGenerator $generator): int
{
try {
$generator->generate();
$this->info('Report updated successfully.');
return self::SUCCESS;
} catch (\Exception $e) {
$this->error($e->getMessage());
return self::FAILURE;
}
}
}
Step 4: Secure Route and Filament Embedded Page
The generated report must remain private. Saving it in the public/ directory would expose server access metrics to the open web. Instead, the report is saved in secure storage, and an authenticated route serves the file.
The controller (App\Http\Controllers\Admin\GoAccessReportController) handles file delivery:
namespace App\Http\Controllers\Admin;
use App\Services\GoAccessReportGenerator;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class GoAccessReportController
{
public function __invoke(GoAccessReportGenerator $generator): BinaryFileResponse
{
abort_unless($generator->reportExists(), 404);
return response()->file($generator->reportPath(), [
'Content-Type' => 'text/html; charset=UTF-8',
'X-Frame-Options' => 'SAMEORIGIN',
]);
}
}
This route is registered inside the Filament panel provider (App\Providers\Filament\AdminPanelProvider), which automatically applies administrative auth middleware to it:
$panel->routes(function (): void {
Route::get('site-analytics/report', GoAccessReportController::class)
->middleware(Authenticate::class)
->name('site-analytics.report');
});
Finally, we display this report inside a custom Filament page template (resources/views/filament/pages/site-analytics.blade.php) by loading the route inside an iframe:
<x-filament-panels::page>
@if ($url = $this->getReportUrl())
<div class="overflow-hidden rounded-xl border border-gray-800 bg-[#0a0a0a]" style="height: calc(100vh - 14rem);">
<iframe src="{{ $url }}" title="GoAccess site analytics" class="h-full w-full border-0" loading="lazy"></iframe>
</div>
@else
<x-filament::section>
<x-slot name="heading">No report yet</x-slot>
<p class="text-sm text-gray-400">Generate a report to view site traffic analytics.</p>
</x-filament::section>
@endif
</x-filament-panels::page>
The Sovereignty of Log Analytics
By leveraging existing Nginx logs, this approach eliminates the need for cookie banners, respects user privacy by default, and avoids importing heavy clientside tracking libraries. Integrating GoAccess reports directly into Filament keeps all operational insights under your control, served securely from a single, lightweight server container.