Features Pricing Docs Blog

Features

Everything Bilis does, and nothing it does not.

Bilis keeps every log line and every span your systems produce on hardware you own, and gets you to the one that explains what went wrong. Nothing about it asks you to trust a format, a vendor, or a landing page: everything below is in the product today, the limits are stated plainly, and the direction — metrics next, with AI that helps you act on what the signals show — is named rather than implied.

01 Ingest

If your stack can emit it, Bilis can take it

Your applications already know how to talk to Bilis. Logs and spans, whatever dialect they speak — getting them in is a configuration change rather than a rewrite. One API key per project, standard OpenTelemetry tables underneath, and a promise that a bad payload never becomes your problem.

  • POST /api/v1/logs

    Success 200

    OTLP ExportLogsServiceRequest, JSON or protobuf

    What every OTel SDK and the Collector already speak.

  • POST /api/v1/traces

    Success 200

    OTLP ExportTraceServiceRequest, JSON or protobuf

    Spans from the same SDKs, on the same key, under the same contract.

  • POST /api/v1/ingest

    Success 202

    Plain JSON: one object or a list of them

    For anything with no OTel exporter. Only the message is required.

  • POST /api/{id}/envelope

    Success 204

    A Sentry SDK envelope

    Configured with a DSN built from a key's public half.

OTLP/HTTP, JSON and protobuf

Standard OTLP, so any exporter you already run works unchanged — including the SDKs that only speak protobuf (Go, Java, .NET, Rust), which Bilis decodes itself with nothing to install. Unknown fields are skipped rather than refused, so a newer collector keeps working against an older Bilis. And if you ever want the binary endpoint off, it is one environment variable.

A plain JSON endpoint, and a Sentry-shaped one

If you would rather not run a collector, /api/v1/ingest takes one JSON object or a list of them; only the message is required. And if your application already reports exceptions through a Sentry SDK, point its DSN here instead and those exceptions land as ERROR records beside the logs that explain them. That is ingestion, not error tracking: no issue list, no grouping, no resolve button.

The coding agent, as well as the app

Claude Code has OpenTelemetry built in, so pointing it here is configuration rather than a plugin: every prompt, tool call, token and dollar it spends lands beside the logs and traces of the application it is editing, with the turn that took nine seconds showing you whether the time went into the model or the tools. Prompt text stays redacted unless you ask for it. Set it up →

Request
# Two records. One of them has no usable message.
curl -X POST https://bilis.example.com/api/v1/ingest \
  -H "Authorization: Bearer bilis_…" \
  -H "Content-Type: application/json" \
  -d '[{"message":"Card declined for order 41902","level":"error","service":"checkout"},{"message":""}]'
Response 202 Accepted
{"accepted":1,"skipped":1}
  • Ingest never returns 400 for a bad payload — bad records are skipped and counted
  • OTLP reports the skipped ones through partialSuccess, still 200
  • A storage failure is 503 with Retry-After, because exporters retry 5xx and drop 4xx
  • Compressed bodies just work — gzip and deflate are inflated for you, up to 32 MB
  • Throttled per key: 1,200 requests a minute, 60 without a key — both configurable
  • Retries are safe: an identical re-sent batch is deduplicated, never double-stored

The full request and response contract →

02 Storage

Your data stays yours, in a format everything can read

Logs live in otel_logs, spans in otel_traces — tables whose column names and types belong to the OpenTelemetry project rather than to us, pinned to an upstream release and re-checked on every upgrade. Any tool that understands that standard, from your scripts to your AI assistants, can read your data today. And if you ever leave, leaving is a SELECT, not a migration project.

Theirs, and left alone

  • Timestamp, TraceId, SpanId, TraceFlags
  • SeverityText, SeverityNumber, ServiceName, Body
  • ResourceAttributes, ScopeAttributes, LogAttributes
  • ScopeName, ScopeVersion, EventName, schema URLs

Ours, and documented

  • A ProjectId column, written only from the authenticated key
  • ORDER BY (ProjectId, Timestamp, ServiceName)
  • PARTITION BY day, so expiry is a partition drop rather than a rewrite
  • A 30-day TTL by default — 90 for trace summaries — and the indexes search runs on

The project a line belongs to is decided by the API key that carried it and never by anything in the payload — a resource attribute named project.id is just an attribute. Writes are queued for throughput, which is why a success means accepted rather than durable; that trade is stated plainly further down.

03 The viewer

From “something is wrong” to the line that explains it

No query language to learn. You ask the way you think — a project, a window of time, a service, a severity — and the answer comes back fast because the storage was designed for exactly that question. There is no way to write a search that quietly scans the whole table.

Filters that match how you think

A time range, one project, one service, and any combination of the six severities — trace, debug, info, warn, error, fatal. Every filter lives in the URL, so the search that found the answer is a link you can paste into the incident channel.

Search the text of every line

Full-text over every log body, case-insensitive, backed by an index built for it. One thing worth knowing before you type: it matches whole words, not substrings — searching timeout finds the line; searching imeou does not.

Every trace, drawn as a waterfall

Spans in order, durations to scale, the failing span marked — you see where the request spent its time instead of guessing. Behind it, the trace list shows each trace's root operation, duration, span count and errors, with a live tail and a per-service latency view for spotting the service that is quietly getting slower.

Two signals, one click apart

A log line that carries a trace id links straight to its waterfall, and a span links back to the logs filtered to that exact trace. The error and the slow query that caused it stop being two investigations.

Watch the deploy as it happens

Leave the live tail open and new lines arrive at the top under the filters you already set. Scroll back to read, and the stream waits for you instead of fighting you for the scroll position.

Everything the line knows

Severity is the only colour on the page, so the eye goes to what matters. Expand any line for its full body and every attribute that arrived with it — trace and span ids included, one click from the waterfall they belong to. Older pages load a hundred lines at a time, as fast as you can read.

A trace waterfall in Bilis: a deploy request across three services, each span's duration drawn to scale, with a detail panel listing the selected span's attributes.
The Bilis log viewer: severity-coloured log lines under a volume histogram, with full-text search, scope, time-window and severity filters above the stream.

04 Dashboard

Open the app, know how things are

One dashboard, the same for everyone, answering the questions you would have built the first one to answer. Nothing to configure and no widget sprawl — the viewer is where the actual work happens.

  • Volume and errors Lines and error-level lines over the last day, against the day before, with the change stated as a percentage — or as nothing at all when there is no prior day to compare against.
  • Recurring failures The error bodies that came back most often, so the loudest thing is at the top rather than buried a thousand lines down.
  • Service liveness Every service that has reported, when it was last seen, and a sparkline of its recent volume and errors. A service that has gone quiet is marked quiet, because silence is the failure mode a log viewer hides best.
  • Retained storage Rows and bytes on disk per project, largest first, read from ClickHouse itself rather than estimated.
  • Ingest budget What each API key has spent against the rate limit in the current minute — the limiter's own rolling counter, labelled as such and never charted over time.

When ClickHouse cannot be reached the cards say so rather than showing zeros, which is a distinction that matters at three in the morning.

Or read none of it yourself: connect the assistant you already code with over MCP and it searches the logs and opens the traces on your behalf, read-only.

05 Projects and keys

A leaked key is a small problem, not a breach

Projects belong to a team, and everything you look at in the app is scoped to the team you are in. Each project issues its own API keys, and a key authorises exactly one thing: writing logs into its own project. It comes in two halves — issued together, revoked together, sharing one rate limit.

Secret half

bilis_…

For collectors, shippers, anything that can set a header. Only a sha256 of it is stored, so the plaintext is shown exactly once and there is no way to read it back.

Public half

bilis_pk_…

Stored in plaintext and always readable on the project page, because it is built into a DSN — a credential in a URL is already disclosed, and hashing it would only make the URL unrecoverable.

Neither half can read a log line, list a project, or touch anything else — so a leaked public key costs you junk in one project's stream, not access to your data. For anything running in a browser there is a second lock that is not on the key at all: a per-project origin allow list, with an empty list meaning no page may post.

How keys are issued and revoked →

06 Running it

You can run the whole thing yourself

Bilis is self-hosted, and that is not a fallback: there is no per-gigabyte bill at the end of a noisy month, and what retention costs you is disk you already pay for. There is a hosted service at bilis.app with a Free plan whose limits are published and anything larger arranged by contact — no checkout, no self-serve billing. Running it yourself stays first-class and free, and has no plan at all.

The entire configuration
CLICKHOUSE_SCHEME=http
CLICKHOUSE_HOST=127.0.0.1
CLICKHOUSE_PORT=8123
CLICKHOUSE_DATABASE=bilis
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=

# Create or update the log table. Idempotent, so it is safe on every deploy.
php artisan clickhouse:migrate

No stack to operate

One app and one database. No Grafana, no Loki, no agent mesh, nothing in between. Fewer moving parts means fewer 3 a.m. surprises — and the whole system fits in one engineer's head.

Nothing exotic to install

No drivers, no PHP extensions, nothing to bolt on. And if the database underneath ever overloads, Bilis fails fast and asks exporters to retry rather than falling over quietly.

Readable before you trust it

The whole thing is open on GitHub, including the schema document that governs the table. You can read exactly what happens to a log line before you point anything at it.

07 Honest limits

The parts a demo would leave out

These are properties of the design, not bugs waiting on a release. They are the things you would find out in week two, so they are here in minute one.

  • An acknowledgement is not durability Rows are inserted with async_insert=1 and wait_for_async_insert=0, so a 200 or 202 means the batch reached the insert buffer. A crash in the window before the flush loses that buffer. The trade is throughput, because small frequent inserts are exactly what ClickHouse handles badly without it. If a line matters more than that, keep a local copy too.
  • Retention is one table-wide TTL Thirty days by default, dropped a whole partition at a time. Spans get the same 30; trace summaries — the rows behind the trace list — keep 90, so a trace outlives its own waterfall. It is a property of the ClickHouse tables, not a per-project setting.
  • Search is token-based Whole tokens, case-insensitively, over the log body. Not substrings and not regular expressions.
  • One node, and no replication Plain MergeTree on a single box. Replication needs Keeper, and a replicated table with unreachable Keeper goes read-only — a failure mode without redundancy on one machine. Replication would not be a backup anyway; back the table up to object storage from day one.
  • Volume control belongs to the sender Bilis stores what arrives. There is no server-side sampling and no ingest-side downsampling, and none is planned — dropping data you deliberately sent is a surprising way to protect a disk you own. Filter and sample in the SDK or collector instead.
  • The rate limit shapes requests, not volume It counts HTTP requests per key, so a well-batched project can still fill the disk without ever seeing a 429. There is no per-project ingest quota yet.

Limits and behavior, including sizing →

08 Where it goes

What is coming, and what stays out

The omissions are named rather than left to be discovered. Some are where Bilis is heading — the observability stack, with AI doing more of the reading. The rest stay out on purpose, and for those there is a tool to use instead.

On the way

  • Metrics The same open standards, on the same box. Logs and traces are already here; metrics complete the picture. Until then, your existing metrics stack keeps its job.
  • Alerting The stack should tell you when to look. Until it does, keep whatever already pages you.
  • AI that reads your logs The point of the roadmap: software that spots the error that matters, explains it, and helps you fix it — on your infrastructure, where your data already is.
  • Dashboards you can shape yourself For now: the built-in overview, and a URL — every filter combination is a link you can bookmark.

Out on purpose

  • Error tracking Sentry itself. The envelope endpoint accepts what its SDKs send, but there is no issue list, no grouping and no resolve button.
  • OTLP over gRPC A Collector, which already bridges that hop. PHP is a poor gRPC server and this will not change.
  • eBPF collection, S3 tiering, replication A larger platform. All three add operating surface a one-box deployment cannot pay for.
  • Self-serve billing A conversation. bilis.app has a Free plan with published limits and no card; a team that outgrows it writes to us and we size something. There is no checkout, and self-hosting stays first-class and free.

Point something at it

A project, a key, and one line of curl is the whole first run — your own logs in the viewer within minutes, and the quickstart does not ask you to install a collector first.