Your apps send OpenTelemetry straight to it.

Set the endpoint and the token on the exporter. Logs, traces and metrics over OTLP, by HTTP or gRPC. No agent on the host and no collector in between; add Vector, Fluent Bit or Alloy for a disk buffer or enrichment. Hosted is this path, our infra.

environment of the app — an OpenTelemetry exporter over HTTP
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:8080
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20${TOPOSCOPE_INGEST_TOKEN}
OTEL_SERVICE_NAME=billing
OTEL_RESOURCE_ATTRIBUTES=service.version=1.4.2
OTEL_LOGS_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
HTTPPOST /v1/logs, /v1/traces, /v1/metrics and /v1/profiles on :8080, as JSON or protobuf, gzip accepted. A 200 says what was not stored: OTLP’s partial success counts the rejected records and names why in one line.
gRPCPort 4319, the same token as authorization metadata. Not OTLP’s usual 4317, which is left to a collector on the same host. It is HTTP/2 without TLS, so behind a reverse proxy it needs a rule of its own.
MetricsGauges, counters and histograms, with no exporter setting: a running total is converted to the amount per interval. A histogram keeps its buckets and reads as count, sum, avg, p50, p90 or p99. After a restart of Toposcope the first total from a running series is only a baseline. No PromQL.
TracesA span keeps the exception recorded on it, with the same e1 as the log row for that exception. It does not become an error row: an exception has to be logged to be hunted.
No collectorWhat an app emits while Toposcope is down for longer than its exporter retries is lost. The release’s vector.yaml is the buffer: OTLP in on 4318 and 4317, 256 MB on disk per signal, each request forwarded as the app sent it.
MarksPOST /v1/marks on the same token. The same id without end_ts is a skip; with end_ts it closes. CI uses deploy-<service>-<tag>.
Version, probesOTEL service.version is stored as version when the sender set no version. POST /v1/probes attaches { service, up: 0|1 } or pulls one status URL; a failed pull stores up=0.
Limits1 MB decoded per request. An OTLP request takes 1,024 log records, spans or profiles, twice an exporter’s default batch of 512; the other endpoints take 500. 50 attributes per event. Busy is 429 with Retry-After: 1 and a failed write is 503; over gRPC both are UNAVAILABLE, which an exporter retries. Syslog UDP has no reply; busy inserts are retried in process.

One Bun app. Two stores. One query path.

The React UI, Hono API, ingest, query and alerts ship in one process. ClickHouse holds signal data; SQLite holds control state.

Toposcope internal architectureSignal inputs enter the Bun app through authenticated APIs and ingest mapping. Signal data is written to raw ClickHouse tables and derived summaries. The query engine reads both. SQLite stores control state used by query and the alert loop, which sends webhooks.TOPOSCOPE · ONE BUN APPCLICKHOUSE · SIGNAL DATASIGNAL INPUTSOTLP · HTTP or gRPClogs · metrics · tracesprofiles · marksJSON / NDJSON · syslogREACT OPERATOR UI · HONO API · AUTHBasic UI/API · Bearer or Basic ingestINGEST + SIGNAL MAPPINGdecode · validate · stampflatten · e1 · map spansbatch caps · backpressureQUERY + INVESTIGATIONkey:value → parameterized SQLraw vs rollup plannerfacets · traces · profilesCONTROL PLANE · SQLITEsaved searches · alert rulesAPI tokens · settingsfield roles · field linksALERT LOOP + WEBHOOKSevery 60 s · saved searchthreshold · cooldown · silencePOST on thresholdRAW SIGNALSlogs · metricsspans · profile_sampleschange_marksDERIVED SUMMARIESminute rollupsattr keys · values · numericmetric summariesTTL 1–365 DAYSWEBHOOK DESTINATIONSHTTP POST after thresholdNORMALIZED WRITESMVs + INDEXESSAVED SEARCHES · FIELD ROLESPOST
Signal inputsOTLP over HTTP or gRPC: logs, metrics, traces, profiles · change marks · JSON / NDJSON · syslog
OTLP / JSON / syslog ↓

TOPOSCOPE · ONE BUN APP

React UI · Hono API · authBasic UI/API · Bearer or Basic ingest
Ingest + signal mappingDecode, validate, flatten, write e1, cap batches, apply backpressure
Query + investigationCompile key:value; choose raw or rollup; fetch traces and profiles
Control plane · SQLiteSaved searches, rules, tokens, settings, field roles
Alert loop + webhooksEvaluate saved searches every 60 s; cooldown, silence, POST
normalized writes ↓ · reads ↑

CLICKHOUSE · SIGNAL DATA

Raw signalslogs · metrics · spans · profile_samples · change_marks
Derived search summariesminute rollups · attr summaries · metric summaries
Write path inputs → signal mapping → raw signals → summaries.Read path raw signals + summaries → query and investigation.Control path SQLite → query + alerts → webhook delivery.
Signal mapping before storageOTLP, JSON, syslog and change marks. Collectors keep enrichment; ingest stamps ts, caps keys, writes e1, and writes ClickHouse.
Raw rows or derived summariesThe query engine compiles key:value to parameterized SQL, then chooses raw tables, minute rollups or attribute summaries.
Control state stays separateSQLite holds saved searches, rules, tokens, settings and field roles. The 60-second alert loop reuses saved searches.

100 million events over seven days, on 2 CPU and 4 GB.

Level, service and host searches across the week return in under 0.1 s, cold or warm. One attribute takes 1.3 s warm and about 3 s cold. A message term is refused across the week; across a day it takes 0.14 s.

RequestReadsColdWarm
/api/search?range=7dlogs_by_minute62 ms18 ms
/api/search?range=7d&q=level:error service:billinglogs_by_minute62 ms55 ms
/api/search?range=7d&q=host:api-1logs_by_minute57 ms56 ms
/api/facets?range=7dlogs_by_minute54 ms30 ms
/api/search?range=7d&q=user_id:u-5logs_attr_values_by_minute3.2 s1.3 s
/api/search?range=7d&q=status:5*logs_attr_values_by_minute3.0 s1.4 s
/api/search?range=7d&agg=p99:duration_mslogs_attr_numeric_by_minute1.6 s1.5 s
/api/search?range=24h&q=timeoutlogs, text index183 ms139 ms
/api/search?range=7d&q=timeoutlogs, text indexrefusedrefused

Refused: a message term across the full week is past the read budget of 20 million rows, 256 MiB or 8 seconds. The newest 100 events still return; the count and histogram do not.

Datasetbun run load:100m on v0.4.8: 100 million synthetic events across 7 days, generated inside ClickHouse with INSERT SELECT. It measures search, not ingest. 9.4 GiB of logs on disk, 11.5 GiB with rollups.
HardwareOne laptop: Apple M3 Max, macOS 15.7, Docker Desktop with 8 CPUs and 16 GB. Two containers at the limits compose.yml ships: ClickHouse 26.3 at 2 CPU / 4 GB, the app at 1 CPU / 512 MB. Image built from the v0.4.8 tag for arm64. The load itself ran with ClickHouse at 6 CPU / 12 GB.
ColdClickHouse restarted and the Linux page cache dropped, then one request. Median of three. The slowest single request was 8.4 s, for status:5*.
WarmThe five requests after each cold one. Median.
TimingThe whole HTTP request, measured on the host. A search returns the newest 100 events, the histogram and the count. 5 October 2026.

Each load profile is in the repo. Run it, then search.

ProfileEventsWindowArrives asRerun with
10k10,0001 hJSON, NDJSON, OTLP and syslog, mixedbun run load
500k500,00024 hJSON, NDJSON, OTLP and syslog, mixedbun run load:500k
10m10,000,0007 dJSON, NDJSON, OTLP and syslog, mixedbun run load:10m
100m100,000,0007 dINSERT SELECT inside ClickHousebun run load:100m
Free is compose on your disk →

Same hunt. We run it.

Hosted early access is planned for Q4 2026. The same product as the free self-hosted edition, operated by us.

No spam · no sales · confirm in your inbox