Log Drains × Grafana Loki — An Observability Pipeline That Collects Vercel·Supabase Logs at Up to 90% Less Cost Than ELK
Right after a deployment, when something feels off but you can't tell exactly what's wrong — have you ever ended up posting "Did anyone catch an error just now?" in Slack? I certainly have, more than a few times. The logs are clearly out there somewhere, but what shows up in the platform dashboard is too fragmented, proper search doesn't work, and you end up waiting for a teammate to spot the problem first.
This post covers how to connect Log Drain and Grafana Loki to collect logs from Vercel·Supabase in real time and query them directly in Grafana with LogQL. Storage costs are up to 90% lower than the ELK stack, and if your team is already using Prometheus, the label schema carries over naturally — so it integrates seamlessly with your existing metrics stack. By the end of this post, you'll be able to ① spin up a Loki + Alloy + Grafana stack locally, ② connect a Log Drain from Supabase or Vercel, and ③ configure everything while avoiding the common label design pitfalls. If you're comfortable with Docker and basic terminal usage, you can follow along right away.
If you're using Promtail: As of March 2, 2026, Promtail has officially reached EOL (End of Life). Security patches have also been discontinued, so migrating to Grafana Alloy is recommended. The Alloy configuration covered in this post should help you chart the migration path.
Table of Contents
- Log Drain — Streaming Logs Out, Not Pulling Them In
- Grafana Loki — Only Labels Are Indexed
- Grafana Alloy — Promtail's Successor (River Language-Based)
- Example 1 (Beginner): Local Observability Stack with Docker Compose
- Example 2 (Intermediate): Supabase → Loki Direct Integration
- Example 3 (Advanced): Vercel → Alloy → Loki — Label Refinement + Trace Integration
- Pros and Cons
- Closing Thoughts
Core Concepts
Log Drain — Streaming Logs Out, Not Pulling Them In
A Log Drain is a streaming pipeline that automatically forwards logs to a pre-configured external endpoint the moment the platform generates them — the exact opposite of manually opening a dashboard to look up logs. You set the destination where logs should flow, and the platform pushes them via HTTP POST automatically.
Log Drain: A continuous streaming pipeline that automatically delivers logs generated by infrastructure or PaaS platforms to external destinations (Loki, Datadog, S3, etc.) in real time. Platforms like Vercel, Supabase, and Render officially support this approach.
In October 2025, Vercel officially launched "Vercel Drains," consolidating not just logs but distributed traces and performance metrics into a single streaming mechanism. Starting March 2026, Supabase expanded Log Drains support to Pro plans. You can now specify Loki directly as a destination, making it possible to build a pipeline without an intermediate collection layer.
Grafana Loki — Only Labels Are Indexed
Loki's design philosophy can be summed up in one sentence: "Don't index log content. Only index labels."
The ELK stack (Elasticsearch + Logstash + Kibana) indexes every field in a log, enabling fast arbitrary-field searches at the cost of explosively large index sizes. Loki takes the opposite approach. It only indexes the set of labels attached to each log stream, and stores the log body as compressed chunks in object storage (S3, GCS, etc.). This is how storage costs can be reduced by up to 90% compared to ELK — the trade-off being that complex aggregations over arbitrary fields are comparatively slower.
The language used to query logs in Loki is LogQL. Inspired by Prometheus's PromQL, it supports label filtering ({app="api", env="production"}), log body search (|= "error"), and metric transformations like rate() and sum by().
The overall pipeline structure looks like this:
[PaaS Platform / Application]
↓ HTTP POST (Loki Push API or OTLP)
[Loki HTTP Endpoint / Grafana Alloy]
↓ Stream labeling + chunk storage
[Loki (Index + Object Storage)]
↓ LogQL query
[Grafana Dashboard]Grafana Alloy — Promtail's Successor
As mentioned, Promtail officially reached end of life on March 2, 2026. Grafana has consolidated all collection functionality into Grafana Alloy — a Grafana distribution of the OpenTelemetry Collector that handles metrics, logs, traces, and profiles through a single agent.
Alloy configuration files are written in the River language. It's similar to HCL (HashiCorp Configuration Language), but a DSL built by Grafana itself. You define pipeline blocks in the form ComponentName "name" { ... } and connect data flow between blocks using forward_to. It can look unfamiliar at first, but once you pick up the pattern, assembling a pipeline declaratively feels quite intuitive.
Existing Promtail YAML configurations can be converted to Alloy format using the following command:
alloy convert --source-format=promtail --output=config.river promtail-config.yamlGrafana Alloy: Grafana's official collection agent based on the OpenTelemetry Collector. The
loki.source.apicomponent lets it receive logs arriving via HTTP drain directly.
Practical Application
Example 1: Local Observability Stack with Docker Compose (Beginner)
Setup: Local machine → Alloy (port 9999) → Loki → Grafana
The fastest way to propose Loki to your team is to run it yourself locally first. I was initially worried it would be complicated to configure, but spinning up Loki + Alloy + Grafana with a single docker-compose turned out to be simpler than I expected.
# docker-compose.yml
services:
loki:
image: grafana/loki:3.0.0 # 3.x is not a valid tag format — check Docker Hub for actual versions
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./local-loki-config.yaml:/etc/loki/local-config.yaml
alloy:
image: grafana/alloy:latest
ports:
- "12345:12345" # Alloy management UI
- "9999:9999" # HTTP drain receive port
volumes:
- ./alloy-config.river:/etc/alloy/config.river
command: run /etc/alloy/config.river
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin⚠️ Local development only: The
auth_enabled: falsesetting in the Loki configuration below disables authentication. In production, you must enable authentication or isolate the Loki endpoint within an internal network.
# local-loki-config.yaml — Minimal configuration for local development
auth_enabled: false # ⚠️ Local only — must be enabled in production
schema_config:
configs:
- from: 2020-10-15
# Note: New clusters can use a past date.
# When adding a new schema to an existing production cluster, set a future date.
store: tsdb # TSDB: Time Series Database Block format used by Loki to store indexes
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24hThe Alloy configuration file can also start in a simple form:
// alloy-config.river — Receive HTTP drain and forward to Loki
// River language: define pipeline blocks as ComponentName "name" { ... }
// and connect data flow between blocks using forward_to
loki.source.api "http_drain" {
http {
listen_address = "0.0.0.0"
listen_port = 9999
}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}Once the containers are up, open Grafana at http://localhost:3000, go to Connections > Data Sources > Loki, enter http://loki:3100 as the URL, and you can start exploring logs immediately. In Grafana Explore, try a simple LogQL query like {job="alloy"} to verify logs are flowing in properly.
| Component | Role | Port |
|---|---|---|
loki.source.api |
Receives drain logs arriving via HTTP | 9999 |
loki.write |
Forwards to the Loki Push API | 3100 |
| Grafana Explore | Run LogQL queries and visualize | 3000 |
Example 2: Supabase → Loki Direct Integration (Intermediate)
Setup: Supabase → Loki (direct connection, no Alloy)
Supabase lets you specify Loki directly as a destination through its configuration UI. Enter the Loki URL under Project Settings > Log Drains, and logs will be automatically flushed at up to 250 events or 1-second intervals and sent with gzip compression.
Structured Metadata must be enabled here. Supabase logs contain many high-cardinality fields such as request IDs and user session IDs. If you receive these as plain labels, the Loki index will explode in size. Structured Metadata is a mechanism that stores these fields in a separate metadata layer without including them in the index. I missed this concept initially and ended up with an index more than 10 times larger than expected — I strongly recommend setting this up from the very beginning.
Note that Structured Metadata only works with schema v13 or later. If you're running an existing cluster, check your schema version first.
# loki-config.yaml — Recommended configuration for Supabase Log Drains integration
auth_enabled: false # ⚠️ In production, enable authentication or isolate the network
limits_config:
allow_structured_metadata: true
max_structured_metadata_entries_count: 500
# Default is 128. Supabase logs have many metadata fields, so increasing this is recommended.
schema_config:
configs:
- from: 2020-10-15
store: tsdb
object_store: filesystem
schema: v13 # Required for Structured Metadata
index:
prefix: index_
period: 24hAutomatic label mapping: The labels Supabase automatically maps are the log source (
auth,storage,realtime, etc.) and the product name. These values have low cardinality, making them well-suited for Loki labels.
Example 3: Vercel → Alloy → Loki — Label Refinement + Trace Integration (Advanced)
Setup: Vercel → Alloy (port 9998, label refinement) → Loki → Grafana Tempo drill-down
Vercel Drains sends runtime, build, and edge function logs to an HTTP endpoint. Alloy's loki.source.api receives these logs, refines the labels, and writes them to Loki.
One interesting aspect is that Vercel automatically includes traceId and spanId in its logs. By storing these fields as Loki Structured Metadata, you can click on a log line in Grafana Explore and drill directly down into Grafana Tempo's distributed traces.
When adding static labels (e.g., env = "production") in Alloy, the correct approach is to use stage.static_labels within the loki.process component. Using __path__ as a source label in loki.relabel is an internal meta-label only valid for local file tailing — it doesn't exist in the HTTP drain context. I wasted quite a lot of time on this when __path__ caused no labels to be applied at all.
// alloy-config.river — Receive Vercel drain + add static labels
// If running alongside Example 1 (port 9999), use a different port
loki.source.api "vercel_drain" {
http {
listen_address = "0.0.0.0"
listen_port = 9998
}
forward_to = [loki.process.add_env_label.receiver]
}
loki.process "add_env_label" {
forward_to = [loki.write.loki_backend.receiver]
stage.static_labels {
values = {
env = "production",
}
}
}
loki.write "loki_backend" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}| Processing Stage | Description |
|---|---|
loki.source.api "vercel_drain" |
Receives Vercel HTTP drain (port 9998) |
loki.process "add_env_label" |
Adds static labels via stage.static_labels |
loki.write "loki_backend" |
Final delivery to the Loki Push API |
Pros and Cons
Pros
| Item | Details |
|---|---|
| Low cost | Up to 90% storage savings over ELK by not indexing log content |
| Operational simplicity | Single binary (monolithic) mode makes it easy to operate even for small teams |
| Prometheus-friendly | Same label schema, PromQL-inspired LogQL, and natural Grafana integration |
| Horizontal scaling | Independent scale-out per component without reindexing |
| OpenTelemetry integration | Native OTLP support makes it easy to build unified metrics·logs·traces pipelines |
| PaaS integration | Officially supported as a Log Drain destination by major platforms like Vercel·Supabase |
Cons and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Label cardinality | Performance degrades sharply with high-cardinality labels | Move user_id, trace_id to Structured Metadata |
| No full-text search | Complex aggregations over arbitrary fields are slower than ELK | For heavy aggregation use cases, consider running ELK in parallel or combining with Grafana Mimir |
| Large-scale self-hosting complexity | Operational burden of distributed Loki + Mimir + Tempo setup is significant | Monolithic mode for small scale; consider Grafana Cloud managed service for large scale |
| Schema version compatibility | Structured Metadata only works with schema v13 or later | Check schema version before migrating existing clusters |
| LogQL learning curve | Teams unfamiliar with PromQL will need time to learn | Can start with Grafana Explore's query builder UI |
Honestly, label cardinality is the issue that comes up most often in practice. If misconfigured, Loki doesn't just slow down — it can die from an OOM error entirely. Taking the time to carefully review your label list when first setting things up can save you from having to rebuild the entire cluster later.
Cardinality: The number of unique possible values a label can have. If the
envlabel can take three values —production,staging,dev— its cardinality is 3, which is low. Something likeuser_id, which has a different value for every user, results in very high cardinality and causes Loki's index size to explode.
The Most Common Mistakes in Practice
- Registering
user_id,request_id,trace_idas labels — Miss this and you'll face an incident immediately. Using high-cardinality fields as labels is the single most common cause of Loki performance degradation. Always store these fields as Structured Metadata instead.
✅ Good labels: app, env, namespace, pod, level, host
❌ Bad labels: user_id, request_id, trace_id, timestamp, order_id-
Keeping Promtail configuration as-is — Promtail is EOL as of March 2026. Security patches have stopped, so use the
alloy convertcommand to migrate. -
Trying to enable Structured Metadata without setting schema v13 and failing — Setting only
allow_structured_metadata: truewithout upgrading the schema version will cause OTel logs to be rejected. You must also setschema: v13for it to work.
Closing Thoughts
Knowing that logs exist somewhere and being able to find them immediately when you need them are two entirely different things. Once you have a Log Drains + Loki pipeline in place, instead of posting "Did anyone catch an error?" in Slack, you can look it up instantly with a single LogQL query in Grafana Explore.
Three steps you can start on right now:
-
Bring up the local stack first — Copy the
docker-compose.ymlfrom Example 1 and rundocker compose up -d. Loki + Alloy + Grafana will come up immediately. You can verify logs are flowing in with a simple LogQL query like{job="alloy"}in Grafana Explore. -
Connect your PaaS platform's Log Drain — If you're using Supabase, add the Loki endpoint under
Project Settings > Log Drains. For Vercel, find theDrainstab in your project settings. Make sure to always set bothallow_structured_metadata: trueandschema: v13inloki-config.yaml. -
Review your label design — Pull out the list of fields you've been attaching to your logs and classify them by cardinality. The pattern where Loki works best is: only fields with a few dozen distinct values or fewer become labels, while everything else stays as Structured Metadata or in the log body.
When you get stuck, the Grafana Community Forum and grafana/loki GitHub Issues are good places to find help.
References
- Grafana Loki Overview | Grafana Official Docs
- Grafana Loki HTTP API | Grafana Official Docs
- Structured Metadata | Grafana Loki Official Docs
- Label Best Practices | Grafana Loki Official Docs
- Label Cardinality | Grafana Loki Official Docs
- OpenTelemetry Ingestion | Grafana Loki Official Docs
- Send Logs to Loki | Grafana Alloy Official Docs
- loki.source.api | Grafana Alloy Official Docs
- Log Drains | Supabase Official Docs
- Introducing Log Drains | Supabase Blog
- Introducing Vercel Drains | Vercel Blog
- Working with Drains | Vercel Official Docs
- The Modern Logging Stack: Loki + Alloy (Why Not Promtail) | rommelporras.com
- Building a 1M/sec Log Ingestion Pipeline with Grafana Loki | Medium
- How to Optimize Loki Label Cardinality | OneUptime Blog
- How to Troubleshoot Loki Rejecting OTel Logs — Structured Metadata | OneUptime
- grafana/loki | GitHub