Build Axiom
KINDAreplaces $25/mosaves $300/yrback to the verdict
An ingest endpoint with per-source rate and size limits, events stored compactly with the fields you actually query indexed, a query interface fast enough to be used during an incident, retention that compacts old data rather than deleting it outright, and alerts on patterns you define.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the ingest endpoint and the query service. Ingest is a hot path, so pick the one you can profile confidently.
Get it go.dev or nodejs.org.
Verify
The toolchain builds and runs a hello world - installfree
Why Handles both the write-heavy ingest and the analytical queries at the volume one team produces.
Get it Install locally or run it in Docker, with enough disk for your intended retention. open ↗
Verify
psql connects and reports 16 or higher - decidefree
Why It determines the limits, the retention period and whether one database is enough. Guessing high wastes money and guessing low means the store stops working during the incident you built it for.
Get it Measure a day of logs from your busiest service. Multiply by your services and by a factor for incidents, since incidents produce far more logs than normal operation.
Verify
A written estimate of events per day and bytes per day exists - have readyfree
Why Field selection, compression ratios and redaction patterns are all only meaningful against real log lines, which are messier and more repetitive than generated ones.
Get it Export a day from a service you run. Include an incident if you have one, since that is the shape the store must survive.
Verify
A real day's logs import and their bytes per event is measured - have readyvaries with volume; this is the recurring cost
Why The storage ceiling and eviction policy exist because disks fill, and a store that fills is a store that stops accepting the logs you need.
Get it Size from your estimate, then add capacity for an incident producing far more than a normal day.
Verify
Free disk comfortably exceeds the configured storage ceiling
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- sources(id, name, token_hash, rate_limit_per_minute, max_event_bytes, enabled, created_at) - events(id, source_id, at, level, message, service, trace_id, attributes_compressed) · indexed fields are explicit, everything else compressed - ingest_stats(source_id, minute, accepted, rejected_rate, rejected_size, bytes) · so a rejecting source is visible rather than silent - saved_queries(id, name, query_json, created_at) - alerts(id, saved_query_id, threshold, window_minutes, last_fired_at, state) - compactions(at, from_date, to_date, events_before, rows_after) · proof that retention ran and what it kept - Which fields are indexed is a deliberate list rather than everything, because that list is the difference between a store that answers quickly and one that does not.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:pass@localhost:5432/logs | Your PostgreSQL connection string. |
DEFAULT_RATE_LIMIT_PER_MINUTE | required | 6000 | Per source. Overridable per source, and the containment for a runaway service. |
MAX_EVENT_BYTES | required | 32768 | Per event. A single enormous event is a different failure from a flood. |
STORAGE_CEILING_GB | required | 200 | Total. Reaching it evicts oldest compacted data rather than refusing ingest. |
COMPACT_AFTER_DAYS | required | 14 | Age at which events compact to counts and samples. |
QUERY_DEFAULT_WINDOW_MINUTES | required | 60 | Default time range, so a casual query never scans everything. |
REDACT_PATTERNS | required | ./config/redact.json | Credential patterns redacted on ingest, before storage. |
The build, in order
Limits before anything else
A source that starts logging in a loop is refused rather than filling the disk.
A token per source, so limits and rejections attribute correctly.
Files
src/auth.goBoth. A single enormous event and a flood of small ones are different failures.
Files
src/limits.goPer source per minute. A silently rejecting source is a source whose operators think it is logging.
Files
src/stats.go
done when · tick each as it passeswatch out- Applying a global rate limit rather than a per-source one means a runaway service starves every other service of ingest, and you lose the logs from the systems that were working. Limit per source, so a misbehaving one is contained rather than contagious.
Storing what you will query
Events are stored compactly and the fields you actually search are fast.
Time, level, service and a trace identifier cover most incident queries. Write down why.
Files
docs/schema.mdOne blob per event. Queryable slowly, which is the right cost for a rare query.
Files
src/store.goBytes per event and query time. Every later claim depends on these numbers.
Files
scripts/generate.go
done when · tick each as it passeswatch out- Indexing every attribute of every event produces an index larger than the data and slows ingest to the point where it becomes the bottleneck. Choose the handful of fields you actually filter by, index those, and accept that a query on an unindexed attribute scans, which is the correct trade at this scale.
Queries you can run during an incident
Finding the relevant events takes seconds when it matters.
Time, level, service, text and trace. Five filters cover most of what an incident needs.
Files
src/query.goRecent by default. Widening is a deliberate action with the cost shown.
Files
src/query.goSo an expensive query is visibly expensive rather than mysteriously slow.
Files
src/query.go
done when · tick each as it passeswatch out- Defaulting a query to all time means every casual search scans the whole store, and during an incident that is exactly when the store becomes unresponsive. Default to a recent window, require an explicit choice to widen it, and show how much was scanned.
Retention that compacts rather than deletes
Old data becomes small rather than absent.
Hourly per service per level, with a bounded sample of full events.
Files
src/compact.goIn one transaction, or the failure mode is losing both.
Files
src/compact.goWhat it processed and what it kept. The proof that retention happened.
Files
src/compact.go
done when · tick each as it passeswatch out- Deleting old logs outright removes your ability to answer whether a problem is new, which is usually the first question during an incident. Compaction keeps the shape of history at a fraction of the size, and the retained samples keep enough detail to recognise a recurrence.
Alerts as saved queries
A pattern you care about notifies you once rather than continuously.
One query language for searching and alerting keeps both honest.
Files
src/alerts.goInto and out of the condition. Not every evaluation.
Files
src/alerts.goQuery, count, window and a link. Enough to judge without opening anything.
Files
src/notify.go
done when · tick each as it passeswatch out- An alert firing on every evaluation while a condition persists produces a message every minute during an incident, which is the moment you least want your attention consumed. Fire on the transition into and out of the condition, and include a link and a count rather than repeating.
Access, secrets in logs, and backupsproduct builder
Logs are readable by the right people and do not contain what they should not.
Enforced in the query rather than the interface. Test with a direct request.
Files
src/access.goBefore storage, for patterns that look like credentials. After storage is too late.
Files
src/redact.goSearch for known secret formats. The results are usually surprising and worth acting on.
Files
scripts/audit.sh
done when · tick each as it passeswatch out- Application logs routinely contain tokens, session identifiers and personal data that nobody intended to log, and once ingested they are searchable by everyone with access. Redact on ingest for the obvious patterns, and grep your own store for a known secret format to find out what you are already storing.
That is the whole plan for Axiom. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Columnar query performance at large scale.
- The integration catalogue of log shippers.
- Managed operations and availability.
- Retention at a scale where storage cost drives the architecture.
- high-performance cloud storage, query engine, integrations, and managed operations
- global probe network
- phone and SMS delivery
- massive retention
- advanced incident response and support
- Structured field extraction from unstructured messages, which makes more of your logs queryable
- A second index field, chosen from what your queries actually filter by after a month
- Sampling of high volume debug events, keeping a proportion rather than rejecting outright
Need the files? The project pack on the verdict page hands your agent the whole brief · more monitoring.