With 7+ years of experience and over 40 production deployments, we deliver robust Loki stacks. Logs scattered across dozens of servers — finding the root cause of a crash turns into a week-long quest. Under peak loads, logs get lost, and searching for an error takes hours. We solve this in a day: set up centralized logging with Grafana Loki and Promtail. Unlike ELK, Loki does not index content, only labels. In practice, this yields up to 70% savings on storage and two to three times less operational overhead. On one project with 50 microservices (10 million requests per day), we reduced time-to-detect from 2 days to 15 minutes. Contact us — we'll complete the setup in 2-3 days. Basic setup cost starts at $500 for the stack (Loki, Promtail, Grafana on Docker; one log source).
Why Loki is Better than Elasticsearch
According to official Grafana Labs data, Loki provides up to 70% storage savings compared to ELK. With 500 GB of logs per day, monthly savings can reach $10,000. Loki stores logs in compressed chunks without an inverted index. Query speed by labels is milliseconds. If you do not need complex aggregation over arbitrary fields, Loki is the right choice. Infrastructure savings can range from $5,000 to $15,000 per month depending on log volume.
| Criteria | Loki | ELK |
|---|---|---|
| Storage cost | Low (compressed chunks) | High (inverted index) |
| Search speed by labels | Fast | Medium |
| Full-text search | Limited | Full |
| Operational complexity | Low (single binary) | High (three stacks) |
| Grafana integration | Native | Via plugin |
How We Deploy the Stack
We use Docker Compose for rapid deployment. The configuration includes three services:
version: '3.8'
services:
loki:
image: grafana/loki:3.0.0
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:3.0.0
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
grafana:
image: grafana/grafana:11.0.0
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=false
- GF_SECURITY_ADMIN_PASSWORD=admin_password
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
volumes:
loki_data:
grafana_data:
Loki Configuration
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 744h # 31 days
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
max_query_length: 721h
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
Promtail Configuration
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: nginx
static_configs:
- targets: [localhost]
labels:
job: nginx
env: production
__path__: /var/log/nginx/access.log
pipeline_stages:
- regex:
expression: '^(?P<ip>\S+) - (?P<user>\S+) \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<bytes>\d+)'
- labels:
status:
method:
- timestamp:
source: timestamp
format: "02/Jan/2006:15:04:05 -0700"
- job_name: laravel
static_configs:
- targets: [localhost]
labels:
job: laravel-app
env: production
__path__: /var/www/app/storage/logs/laravel.log
pipeline_stages:
- multiline:
firstline: '^\[\d{4}-\d{2}-\d{2}'
max_wait_time: 3s
- regex:
expression: '^\[(?P<timestamp>[^\]]+)\] (?P<env>\w+)\.(?P<level>\w+): (?P<message>.*)'
- labels:
level:
env:
- timestamp:
source: timestamp
format: "2006-01-02 15:04:05"
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: [__meta_docker_container_name]
target_label: container
- source_labels: [__meta_docker_container_log_stream]
target_label: logstream
Deployment in Kubernetes
For Kubernetes, use Helm charts: `helm install loki grafana/loki-stack` — this will deploy Loki, Promtail, and Grafana with default settings. For production, adjust retention, labels, and pipeline_stages via values.yaml.How to Integrate Laravel with Loki via Monolog
We write a custom Monolog handler that sends log entries to Loki via HTTP API. It works fire-and-forget — does not block the request.
// app/Logging/LokiHandler.php
namespace App\Logging;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\LogRecord;
class LokiHandler extends AbstractProcessingHandler
{
public function __construct(
private string $lokiUrl,
private array $labels = []
) {
parent::__construct();
}
protected function write(LogRecord $record): void
{
$timestamp = (string)($record->datetime->getTimestamp() * 1_000_000_000);
$payload = [
'streams' => [[
'stream' => array_merge($this->labels, [
'level' => $record->level->getName(),
'channel' => $record->channel,
]),
'values' => [[$timestamp, $record->formatted]],
]],
];
$context = stream_context_create(['http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode($payload),
'timeout' => 1,
]]);
@file_get_contents("{$this->lokiUrl}/loki/api/v1/push", false, $context);
}
}
// config/logging.php
'loki' => [
'driver' => 'monolog',
'handler' => App\Logging\LokiHandler::class,
'with' => [
'lokiUrl' => env('LOKI_URL', 'http://loki:3100'),
'labels' => [
'app' => 'web-app',
'env' => env('APP_ENV', 'production'),
],
],
],
Step-by-Step Setup for Collecting Laravel Logs
- Create the
LokiHandlerclass as shown above. - Register the
lokichannel inconfig/logging.php. - Add the
LOKI_URLenvironment variable in.env. - Verify log delivery via LogQL:
{job="laravel-app"}. - Optionally configure pipeline_stages for parsing multi-line exceptions.
How to Write LogQL Queries
LogQL is similar to PromQL. Basic patterns:
# All Laravel errors in the last hour
{job="laravel-app", level="error"} |= "Exception"
# Nginx 5xx
{job="nginx"} | json | status >= 500
# Error rate per minute
rate({job="laravel-app", level="error"}[1m])
# Top slow requests (if request_time is in the log)
{job="nginx"}
| regexp `request_time=(?P<rt>[0-9.]+)`
| unwrap rt
| quantile_over_time(0.95, [5m]) by (path)
# Error count by type
sum by (level) (
count_over_time({job="laravel-app"}[5m])
)
How to Set Up Alerts on Error Spikes
Create an alert rule via provisioning YAML. Example condition: sum(rate({job="laravel-app", level="error"}[5m])) > 0.1. The alert fires if threshold exceeded for over 2 minutes. Configured in grafana/provisioning/alerting/alert_rules.yml. Also add an alert on sudden spike of Nginx 5xx errors.
Process and Timelines
| Stage | What We Do | Duration |
|---|---|---|
| Analysis | Identify log sources, labels, retention | 0.5 day |
| Design | Choose Promtail, Loki configuration, datasource | 0.5 day |
| Implementation | Deploy stack, configure collection, dashboards, alerts | 0.5 day |
| Testing | Verify correct delivery, query functionality | 0.5 day |
| Deployment | Deliver documentation, credentials, train team | included |
Total: 2-3 days for the full stack. Pricing starts at $500 for the basic stack (Loki, Promtail, Grafana on Docker; one log source). Get a consultation on your project — we'll assess and propose a solution.
What's Included
- Deployed Loki + Promtail + Grafana stack in Docker Compose or on bare metal.
- Configuration of retention, labels, and pipeline_stages for your sources.
- Custom Monolog handler for Laravel (or similar for other frameworks).
- Grafana dashboards visualizing errors, trends, top endpoints.
- Alert rules on error spikes, 5xx, slow queries.
- Operations and recovery documentation.
- Handover and team training (1 hour).
- One-month warranty of correct stack operation after deployment.
Common Issues and Their Solutions
- Incorrect retention setup — compactor forgotten, logs not deleted. Always check
compactor.retention_enabled: true. - Missing pipeline_stages — labels not parsed, filtering difficult. For Laravel, always use multiline stage.
- Too many labels — each new label creates an index, slowing writes. Limit labels to 5-7 per source.
Learn more about logging on Wikipedia. Order the setup and get a consultation on your project.







