A three-person startup comes to you with a $4 000/month Datadog bill and asks how to cut it. A 500-person enterprise comes with a $150 000 ELK licence and asks the same. Both problems have the same root cause: observability gets billed per byte you ingest, and once your app emits logs, you’re locked in.
This post shows how two containers — Vector (52 MB) and PostgreSQL with TimescaleDB — replace most of what Datadog / Splunk / Elastic sells. No vendor, no per-byte tax, your data stays on your infrastructure.
The architecture in one diagram
┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ Logs (apps) │──┐ │ │ │ │
└──────────────┘ │ │ │ │ Postgres + time- │
├─▶│ Vector (52 MB) │─────▶│ scaleDB hyper- │
┌──────────────┐ │ │ musl static bin │ │ tables │
│ Metrics │──┤ │ │ │ │
│ (Prometheus) │ │ │ VRL transforms │ │ + pgvector for │
└──────────────┘ │ │ routing, buffer │ │ log embeddings │
│ │ │ │ │
┌──────────────┐ │ └────────┬─────────┘ └─────────┬─────────┘
│ syslog, │──┘ │ │
│ systemd, │ │ │
│ journald │ ▼ ▼
└──────────────┘ ┌──────────────────┐ ┌───────────────────┐
│ Also: S3, Kafka,│ │ Query: Grafana, │
│ New Relic, DD │ │ psql, SQL alerts │
│ (parallel fan- │ │ │
│ out during │ │ │
│ migration) │ │ │
└──────────────────┘ └───────────────────┘
Vector is the pipeline. PostgreSQL is the store. You already know SQL. The bill is electricity.
Vector in 60 seconds
Vector (by Datadog, open-source MPL-2.0) is a rust-written pipeline for logs, metrics, and traces. Think “fluentd/logstash replacement but written in 2020 and 10× faster.”
Our image:
docker pull ghcr.io/oorabona/vector:latest-alpine
# 52 MB, multi-arch, static musl binary
- FROM scratch-ish — alpine with the static vector binary downloaded at build time
- Multi-arch (amd64, arm64)
- Non-root (uid 1000)
- HEALTHCHECK via Vector’s
/healthendpoint - Auto-tracked from vectordotdev/vector releases (with a regex that correctly ignores their
vdev-v*CLI subproject releases — story here)
A minimal config: Nginx logs to Postgres
# vector.toml
data_dir = "/var/lib/vector"
[sources.nginx]
type = "file"
include = ["/var/log/nginx/*.log"]
ignore_older_secs = 86400
read_from = "end"
[transforms.parse]
type = "remap"
inputs = ["nginx"]
source = '''
. = parse_regex!(
.message,
r'^(?P<remote>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<bytes>\d+)'
)
.timestamp = parse_timestamp!(.time, "%d/%b/%Y:%H:%M:%S %z")
.status = to_int!(.status)
.bytes = to_int!(.bytes)
'''
[sinks.postgres]
type = "postgres"
inputs = ["parse"]
endpoint = "postgres://vector:${POSTGRES_PASSWORD}@db:5432/observability"
table = "nginx_logs"
batch.max_events = 1000
batch.timeout_secs = 5
Create the destination table:
CREATE TABLE nginx_logs (
timestamp TIMESTAMPTZ NOT NULL,
remote TEXT,
method TEXT,
path TEXT,
status INT,
bytes BIGINT
);
SELECT create_hypertable('nginx_logs', 'timestamp', chunk_time_interval => INTERVAL '1 day');
CREATE INDEX ON nginx_logs (status, timestamp DESC);
That’s it. Nginx access logs flow into a TimescaleDB hypertable, automatically partitioned by day. Queries:
-- Top 10 404 paths in the last hour
SELECT path, count(*) as hits
FROM nginx_logs
WHERE timestamp > now() - interval '1 hour'
AND status = 404
GROUP BY path
ORDER BY hits DESC
LIMIT 10;
No Kibana, no query language to learn. SQL.
Why pair Vector with our Postgres
We ship a postgres:18-alpine-full variant with:
- TimescaleDB for time-series compression (10×–100× size reduction on old chunks)
- pgvector for embedding-based log search (recent Datadog-ish feature: “find logs similar to this one”)
- pg_cron for scheduled rollups (“aggregate hourly stats overnight”)
- paradedb / pg_search for BM25 full-text log search — the Elasticsearch use case without Elasticsearch
One container, one database, every observability workload.
Real use cases
1. Log search at scale
With pg_search on the message column:
CREATE INDEX log_search ON nginx_logs
USING bm25 (timestamp, path, message) WITH (key_field='timestamp');
-- "Find error logs mentioning database"
SELECT timestamp, message
FROM nginx_logs
WHERE message @@@ 'error AND database'
ORDER BY paradedb.score(timestamp) DESC
LIMIT 50;
2. Anomaly detection with embeddings
-- Assume an external pipeline wrote embeddings to log_embeddings(log_id, embedding vector(768))
-- "Find logs similar to this error"
WITH needle AS (
SELECT embedding FROM log_embeddings WHERE log_id = 12345
)
SELECT l.timestamp, l.message
FROM log_embeddings e
JOIN nginx_logs l ON l.id = e.log_id
CROSS JOIN needle
ORDER BY e.embedding <=> needle.embedding
LIMIT 20;
3. Alerts without a separate system
-- Every minute, check if p99 latency exceeded threshold
SELECT cron.schedule(
'latency-alert',
'* * * * *',
$$
INSERT INTO alerts (fired_at, rule, detail)
SELECT now(), 'p99_too_high', jsonb_build_object('p99', p99)
FROM (
SELECT percentile_cont(0.99) WITHIN GROUP (ORDER BY response_ms) p99
FROM nginx_logs
WHERE timestamp > now() - interval '5 min'
) x
WHERE p99 > 2000
$$
);
PagerDuty trigger = webhook from a Postgres trigger on the alerts table. 20 lines of SQL. No Grafana Mimir.
Size story
| Component | Size | vs. “standard” stack |
|---|---|---|
| Vector | 52 MB | vs. Logstash ~800 MB (JVM) |
| Postgres+full | 242 MB | vs. Elasticsearch 700 MB + Kibana 1 GB |
| TOTAL | ~294 MB | vs. ~2.5 GB |
Before anyone says “but you still need Prometheus, Grafana, etc.”: you don’t. Vector does metric scraping (Prometheus source + remap), Postgres does storage, and Grafana (if you want a UI) has a Postgres data source. The whole stack is 5 containers, ~500 MB total.
Migration from Datadog
Vector has a datadog_agent source that accepts DD Agent protocol. Run in parallel:
[sources.dd_proxy]
type = "datadog_agent"
address = "0.0.0.0:8080"
# Original sink: keep sending to Datadog
[sinks.datadog]
type = "datadog_logs"
inputs = ["dd_proxy"]
default_api_key = "${DATADOG_API_KEY}"
# New sink: also send to Postgres
[sinks.postgres_logs]
type = "postgres"
inputs = ["dd_proxy"]
endpoint = "postgres://..."
table = "all_logs"
Point your applications’ DD_API_ENDPOINT to Vector instead of api.datadoghq.com, verify the Postgres side is collecting, then turn off the Datadog sink. Your apps don’t need a single code change.
Limitations
- Traces — Vector can route OTEL traces to Postgres but the querying story is weak. Tempo or Jaeger in parallel still makes sense for trace search.
- Log retention > 90 days at high volume — Postgres hypertable compression handles this (tens of GB → GB with timescale_toolkit), but at petabyte scale you want a dedicated cold store (S3 via Vector’s S3 sink).
- Query latency — Elasticsearch is faster for interactive log search on very large datasets. Postgres catches up with proper indexing, but if you’re running > 10 TB of indexed logs, benchmark.
- Alerting UI — no Kibana rule builder. You write SQL. If your team can’t write SQL, this stack is the wrong call.
For 95% of companies, though, this stack is under-specced for the bills they’re paying elsewhere.
TL;DR
# Vector: the pipeline
docker pull ghcr.io/oorabona/vector:latest-alpine # 52 MB
# Postgres with everything: the store
docker pull ghcr.io/oorabona/postgres:18-alpine-full # 242 MB
Both on the dashboard.
If this stack saves you a Datadog renewal, ⭐ the repo. We add features based on what the star count tells us people actually want.