Why Real-Time Log Analytics Matters for Laravel Microservices

You're sprinting a checkout feature. You push, CI spins containers, and five minutes later a user complains about a five-second lag. You hop onto host A, tail service-a.log—nothing. Switch to host B, grep "error"—a stack trace that doesn't match the request. By the time you stitch the timeline together, revenue is already slipping.
That's distributed logging in its raw form. Latency spikes and silent timeouts hide because each service logs in isolation. Real-time aggregation flips the script; every event lands in a single searchable store the instant it's emitted. You can follow a request ID across services, spot a sudden wave of 500s, and fire alerts before users notice.
Enter ELK. Elasticsearch indexes each line, Logstash normalises the payload, Kibana visualises it instantly. Filebeat on each Laravel container ships logs over TLS—no custom agents required. The result: one pane of glass, one query, the full request journey from API gateway to DB microservice. Debugging becomes a click, not a hunt.
Architecture Overview & Technical Prerequisites

Data flow, simplified:
Laravel Service (Docker) → Filebeat → Logstash → Elasticsearch → Kibana
Each container runs the official Filebeat binary. Filebeat reads storage/logs/laravel.log, ships JSON-encoded events to Logstash on port 5044. Logstash applies a Grok filter, enriches with service name and container ID, then writes to Elasticsearch. Kibana sits on top, serving dashboards and alerts.
What you need:
- Docker ≥ 20.10, Docker-Compose ≥ 2.0.
- Elasticsearch 7.17.x (last stable 7 series, works with Laravel 10).
- Logstash 7.17.x and Kibana 7.17.x – keep versions in lockstep.
- PHP 8.1+ with
ext-jsonandext-mbstring. - Filebeat 7.17.x – same major version as Elasticsearch.
- Network: expose 5044 (beats), 9200 (ES API), 5601 (Kibana). Use a dedicated Docker network so containers talk without opening everything to the host.
Give the host at least 4 GB RAM; Elasticsearch will hog most of it. On a laptop, cap the JVM heap at 2 GB (see tuning later).
Step-by-Step Core Setup
First, spin up the ELK stack with Docker-Compose. Create docker-compose.yml in the project root:
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.10
container_name: es
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms2g -Xmx2g
ulimits:
memlock:
soft: -1
hard: -1
ports:
- "9200:9200"
networks:
- elk
logstash:
image: docker.elastic.co/logstash/logstash:7.17.10
container_name: logstash
volumes:
-./logstash/pipeline:/usr/share/logstash/pipeline:ro
ports:
- "5044:5044"
depends_on:
- elasticsearch
networks:
- elk
kibana:
image: docker.elastic.co/kibana/kibana:7.17.10
container_name: kibana
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- elk
networks:
elk:
driver: bridge
Next, add a minimal Logstash pipeline. Create logstash/pipeline/laravel.conf:
input {
beats {
port => 5044
}
}
filter {
json {
source => "message"
target => "log"
}
mutate {
add_field => {
"service" => "%{[host][name]}"
}
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "laravel-%{+YYYY.MM.dd}"
user => "elastic"
password => "changeme"
}
stdout { codec => rubydebug }
}
Now configure Laravel to emit JSON. In config/logging.php add a new channel:
'elk' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'formatter' => Monolog\Formatter\JsonFormatter::class,
],
Make it the default inside containers:
LOG_CHANNEL=elk
Install Filebeat inside each Laravel Docker image. Extend your Dockerfile:
FROM php:8.1-fpm
#... your usual extensions...
# Install Filebeat
RUN curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.17.10-amd64.deb \
&& dpkg -i filebeat-7.17.10-amd64.deb \
&& rm filebeat-7.17.10-amd64.deb
# Copy Filebeat config
COPY filebeat.yml /etc/filebeat/filebeat.yml
# Ensure the log dir exists
RUN mkdir -p /var/www/html/storage/logs
Filebeat config (filebeat.yml) points to Logstash:
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/www/html/storage/logs/*.log
json.message_key: log
json.keys_under_root: true
output.logstash:
hosts: ["logstash:5044"]
Fire it up:
docker compose up -d
Open Kibana, create an index pattern laravel-*, and you'll see Laravel JSON events streaming in real time. That's the core loop.
Advanced Configuration & Performance Tuning
Now that the pipeline works, let's tighten it.
Ingest Pipelines for Enrichment
Instead of doing everything in Logstash, push some work to Elasticsearch's ingest node. Create a pipeline that extracts the request ID and adds a timestamp field:
PUT _ingest/pipeline/laravel-pipeline
{
"description": "Parse Laravel logs",
"processors": [
{
"grok": {
"field": "message",
"patterns": ["%{TIMESTAMP_ISO8601:log_timestamp} %{LOGLEVEL:level} %{DATA:msg}"]
}
},
{
"date": {
"field": "log_timestamp",
"formats": ["ISO8601"]
}
},
{
"rename": {
"field": "request_id",
"target_name": "trace.id"
}
}
]
}
Update Logstash output to reference the pipeline:
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "laravel-%{+YYYY.MM.dd}"
pipeline => "laravel-pipeline"
}
}
Custom Grok Patterns
If you're using Laravel's default log format, the built-in patterns cover most cases. I often add a pattern for the user_id field my app injects:
POSTGRESQL_LOG %{TIMESTAMP_ISO8601:timestamp} \[%{DATA:level}\] %{GREEDYDATA:msg} user_id=%{INT:user_id}
Save it under logstash/pipeline/patterns/custom and reference it:
grok {
patterns_dir => ["./patterns"]
match => { "message" => "%{CUSTOM_LOG}" }
}
Index Lifecycle Management (ILM)
Logs explode fast. Define a policy that rolls over daily and deletes after 30 days:
PUT _ilm/policy/laravel-logs
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": "1d",
"max_size": "50gb"
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
Attach it to the index template:
PUT _template/laravel
{
"index_patterns": ["laravel-*"],
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"index.lifecycle.name": "laravel-logs",
"index.lifecycle.rollover_alias": "laravel"
}
}
Batch Size & Pipeline Workers
Filebeat ships events in batches. The default bulk_max_size of 50 works for low traffic, but bump it to 200 if you see "backoff" warnings in Logstash logs. In logstash.yml set:
pipeline.workers: 4
pipeline.batch.size: 125
pipeline.batch.delay: 50
Four workers match a 4-core VM nicely; adjust based on CPU.
JVM Heap Sizing
Elasticsearch runs on the JVM. Never let it stay at the default 1 GB on a production box. Set ES_JAVA_OPTS=-Xms4g -Xmx4g in Docker Compose if you have 8 GB RAM. For dev, keep it at 2 GB to avoid OOM on a laptop.
Kibana Dashboards
Import the community Laravel dashboard from the ELK-Stack-with-Laravel repo. It visualises request latency, error rates per service, and top exception messages. After importing, set the time filter to "Last 5 minutes" and you have a real-time health board.
If you need alerts, use Kibana's rule engine. Create a rule that fires when status >= 500 for a given service exceeds five hits in a minute. Wire the action to a Slack webhook – you'll get a ping before users notice the outage.
Critical Production Pitfalls & Diagnostic Gotchas
Deploying to production flips a switch. Your fancy dev-only logger suddenly behaves like a teenager on caffeine. Here's what actually bites you when you run real-time log analytics for Laravel microservices, plus the commands that surface the trouble fast.
Log format drift – One service still uses the old formatter while the rest have moved on. Elasticsearch starts rejecting docs; the mapping no longer matches. curl -XGET 'http://es-node:9200/_cat/indices?v' | grep _rejected will show a spike. Compare a good _source with a rejected one, then sync Monolog\Formatter\JsonFormatter across config/logging.php. Re-index or spin up a fresh index with the corrected mapping.
Back-pressure on Logstash/Beats – Traffic spikes fill Filebeat's internal queue. You'll see "backoff" warnings. Check the queue size:
journalctl -u filebeat -f | grep "queue full"
Raise queue.mem.events, add another Logstash worker, or tweak bulk_max_size to ship larger batches.
Disk-full Elasticsearch – A single node can run out of space if you never roll over indices. The cluster health goes red; new logs vanish. Verify:
curl -XGET 'http://es-node:9200/_cluster/health?pretty'
If you get "status":"red", free space or add a data node. In production I always enable an ILM policy that rolls over daily and prunes anything older than 30 days.
Time-zone mismatches – Laravel logs in UTC, Kibana often defaults to your local zone. Queries that cross midnight look empty. Check Kibana's Advanced Settings and compare against a sample log:
GET /logs-*/_search
{
"_source": ["@timestamp"],
"size": 1,
"sort": [{ "@timestamp": "desc" }]
}
Force dateFormat:tz to UTC. Consistency wins.
Quick remediation checklist
- Schedule the curl health checks in cron.
- Standardise Monolog JSON format in
config/logging.php. - Enable ILM or Curator for index lifecycle.
- Boost Filebeat queue settings before traffic spikes.
- Lock Kibana timezone to UTC.
Comparison & Decision Matrix
| Solution | Cost | Setup Complexity | Real-Time Guarantees | Laravel Integration | Scaling |
|---|---|---|---|---|---|
| ELK (Elasticsearch + Logstash + Kibana) | Open-source, but hosting & storage add up ($/GB) | Medium – pipelines, ILM, Beats needed | Near-real-time (sub-second to a few seconds) | Native Monolog JSON, Filebeat tailing | Horizontal via data nodes; shard mgmt required |
| Loki + Grafana | Free core, optional Grafana Cloud fees | Low – only promtail config | Seconds-level, label-index dependent | Community packages exist, less plug-and-play | Chunked storage scales; query latency grows with label cardinality |
| Graylog | Free OSS, enterprise adds cost | Medium – MongoDB & Elasticsearch underneath | Sub-second to a few seconds, pipeline dependent | Syslog/beat inputs; need custom Laravel JSON formatter | Clustered nodes, but Elasticsearch stays bottleneck |
| Splunk (cloud or on-prem) | License-based, pricey at high ingest rates | High – forwarders, indexing policies, provisioning | Real-time (sub-second) with heavy indexing overhead | HTTP Event Collector works; thin wrapper required | Cloud auto-scales; on-prem needs index clustering |
If you already own an ELK cluster, stick with it. Need a lighter footprint and already run Prometheus? Loki feels natural. Graylog shines when you crave a SIEM-like UI without the Elasticsearch price tag. Splunk is overkill unless compliance forces you.
FAQ – High-Intent Technical Questions
How do I ship logs from queued jobs that run on separate workers?
Queued jobs inherit the same logger instance, but they execute in a different process pool. The trick: inject the request ID into the job payload and re-apply it in handle. Example:
class ProcessOrder implements ShouldQueue
{
public $tries = 3;
public function __construct(public string $requestId) {}
public function handle
{
Log::withContext(['request_id' => $this->requestId]);
Log::info('Processing order', ['order_id' => $this->orderId]);
}
}
Tell Filebeat to watch storage/logs/laravel-queue.log. You'll see the same request_id flowing through Elasticsearch.
Can I filter logs by request ID in Kibana without writing a full query?
Yes. Save a search that uses request_id as a quick filter. In Discover, type request_id:"{{your-id}}", click "Add to filter", then pin the search to a dashboard. Paste a new ID and you instantly get a live view across services.
What's the best way to secure Elasticsearch in a production microservice environment?
Don't expose it directly. Put NGINX or Traefik in front, enable TLS, require client certs. Turn on xpack.security.enabled: true (basic security works even in OSS). Create a role that only allows indices:write for Logstash/Filebeat and a read-only role for Kibana users. Lock the network so only Beats and Logstash IPs can hit the ES port.
How should I handle log rotation without losing entries?
Laravel's single channel writes to storage/logs/laravel.log. If you rotate with logrotate, use copytruncate so the file descriptor stays valid for Filebeat. Example /etc/logrotate.d/laravel:
/var/www/app/storage/logs/*.log {
daily
rotate 7
compress
missingok
copytruncate
notifempty
}
Copytruncate prevents the "file not found" moment that would otherwise drop a batch of lines.
Is it safe to push logs directly from Laravel to Logstash over TCP?
It works, but you lose the resilience Beats give you. Beats buffer locally, handle back-pressure, retry automatically. Going straight TCP means you must code your own retry logic and accept dropped logs during network hiccups. For most teams the extra hop through Filebeat is worth the reliability.
Operational Verification Checklist & Next Steps
- Hit each microservice's
/healthendpoint; look for alog_pipelineflag and a200status. - Run
GET /_cat/indices?v– all indices should begreenoryellow, neverred. - Validate Filebeat on every host:
systemctl status filebeatand verify "Harvesting" lines appear. - Confirm your ILM policy has rolled over at least once; query
_ilm_explainon a recent index. - Set a Kibana alert for
logstash.errors > 0in the last five minutes. - Open the "Live Requests" dashboard; make sure request IDs appear across all services.
Next phase? Add distributed tracing (OpenTelemetry) to correlate logs with spans, and hook the alert webhook into your incident-response Slack channel. That closes the loop: logs, metrics, traces, alerts.
Need a hand tailoring the stack to your Laravel microservices? Our observability service can design, implement, and hand off a production-grade pipeline. Drop us a line and we'll brew a solution that fits your traffic patterns.