Repository navigation
v1.6.1
A release about speed and getting the details right: the Logs page histogram and totals come from per-minute counts on TimescaleDB, ClickHouse and MongoDB, facets work on projects with tens of millions of logs, the client IP behind a reverse proxy is the real one, database and Redis passwords can contain any character, and project health no longer emails on every quiet night.
Before upgrading: migrations 075 to 077 run on start. On ClickHouse the first start migrates the traces table and needs every 1.6.0 backend and worker stopped first (see Changed). Behind a reverse proxy, set TRUST_PROXY (1 for one proxy, 2 with Cloudflare in front).
Added
- Traffic map from client IPs: the geo map dashboard panel needed the fields a GeoIP pipeline step writes at ingestion, so projects without that step showed "No geo data" even though their logs carry the client IP. When those fields are missing, the panel now locates the most frequent client IPs of the time range (up to 10,000 distinct addresses) with the GeoLite2 database the server already uses for security enrichment, and sums them per country or per place. It works on existing logs, stores nothing and sends nothing outside the server. The IP is read from a metadata field such as
client_ip(what the Nginx and Apache parsers write),iporremote_addr, or from the field set in the new "Client IP field" panel setting (ipField); forwarded-for lists andip:portvalues are understood. Private and unknown addresses are skipped, and the panel says where its locations come from. - Instance admin pages for extensions: extensions can now add pages for instance administrators, next to the ones LogTide ships. On the backend,
api.auth.requireInstanceAdminguards a route for instance administrators (re-checked on every request, so an administrator who loses the role or is disabled is refused right away),api.admin.audit()records their actions in the audit log asext.admin_actionwith the real administrator as actor, andapi.orgs.getMany()looks up to 500 organizations at once. On the frontend,adminNavadds pages to the admin navigation and the command palette (administrators only), andadminOrgPanelsadds sections to the admin organization page; admin pages live under/dashboard/admin/ext/<name>/and inherit the admin guard. The extension API stays at version 1, and an instance without extensions looks and behaves exactly as before. Seedocs/extensions.md. - Database and Redis as separate settings (discussion #317): the backend and the worker accept
DB_HOST,DB_PORT(default 5432),DB_USER,DB_PASSWORDandDB_NAME, andREDIS_HOST,REDIS_PORT(default 6379) andREDIS_PASSWORD, and build the connection URLs themselves with every part percent-encoded, so a password with#,%,@,/,?or:works as it is. Before, the compose files putDB_PASSWORDintoDATABASE_URLunencoded, and such a password stopped the backend at start withCannot read properties of undefined (reading 'searchParams').DATABASE_URLandREDIS_URLare still supported and take precedence when set, for existing installs and managed databases; an empty value counts as unset.DB_HOSTwithoutDB_USERorDB_NAMEstops the server at start with an error naming what is missing.
Performance
- Log volume on MongoDB from per-minute counts: MongoDB has no incremental aggregate, and counting logs in its time-series collection unpacks every one of them (2.8 seconds for the histogram and 0.6 seconds for the count of 5 million logs). LogTide now keeps per-minute counts per project, level and service next to the logs collection (
logs_volume_minutes, withlogs_volume_stateandlogs_volume_dirty), filled by the worker every minute up to five minutes ago (refreshLogVolume; about 5 seconds for 5 million logs the first time, a few milliseconds after that). Histograms and counts of searches that filter only by project, level or service read them for those minutes: 71 ms and 61 ms. Logs written with an older timestamp and deletes (retention, project purge) mark the minutes they touch before writing, and those minutes are read from the logs until the next refresh has counted the finished write, so the numbers match the logs; several workers can refresh at once without double counting. Batches of logs newer than two minutes skip this bookkeeping. If the counts cannot be read, the logs are read as before. - Log volume on ClickHouse from a minute projection: the logs table gets the aggregate projection
proj_minute_volume(log count per minute, project, level and service), added and materialized in the background on the next start (2 seconds for 36 million logs; parts not yet materialized are read as logs meanwhile). Histograms and counts of searches that filter only by project, level or service read it for the whole minutes of the range and read the partial minutes at the edges as logs: 22 ms instead of 0.84 seconds and 12 ms instead of 0.42 seconds over 36 million logs. ClickHouse keeps the projection inside every part, so inserts, merges and retention deletes keep the numbers exact. - Log volume on TimescaleDB from a per-minute aggregate: the histogram and the total of the Logs page scanned every log of the time range (6 seconds for the histogram and 2 seconds for the count of 5 million logs). A new continuous aggregate
logs_minute_stats(migration 075, one row per minute, project, level and service) now answers the whole minutes of the last 30 days when the search filters only by project, level or service; the edge minutes, the newest minutes, anything older and any minute TimescaleDB still lists as changed since its last refresh (late logs, backfills, retention deletes) are read from the logs table, so the numbers are exactly the same as before. The same requests take 30 ms and 6 ms. Searches with text, metadata, host, trace or session filters work as before. The aggregate is filled in the background by its refresh policy (about 6 seconds for 5 million logs, once); until then requests cost what they did. - Log pages on MongoDB: the logs collection is a time-series collection, where only a sort on
timealone runs one bucket at a time; the log list sorted on time and id, which made MongoDB sort the whole time range for every page (about 5 seconds for 5 million logs, 10 seconds without a filter). Pages now read on a time-only sort and order logs sharing a timestamp by id in memory, with the same order and cursors as before: about 0.4 to 0.6 seconds. - Facets on big projects: the facet sidebar of the Logs page (
GET /api/v1/logs/facets) counted every log of the time range and read each metadata key from every row, so on large projects it took longer than the request could wait: over 30 seconds for 36 million logs on ClickHouse, 23 seconds for 5 million on TimescaleDB and over 60 seconds for 5 million on MongoDB. Facets are now counted over the newest matching logs, at most 100,000 of them, taken from the newest 2,000,000 logs of each project in the range; smaller ranges are still counted in full. The same requests now take about 0.5, 1.3 and 2 seconds. The response gainssampleSize(how many logs were counted) and the sidebar says "Top values of the newest N of M matching logs" when the facets cover only part of them. The affected services in the alert preview come from the same facets. In@logtide/reservoir,facets()acceptssampleSizeandscanLimit(at most those defaults) and returnssampleSize. - Field discovery on big projects:
GET /api/v1/logs/fields, which picks the metadata keys shown as facets, sorted the metadata of the whole time range on ClickHouse (8 to 15 seconds for 36 million logs) and on MongoDB (5 seconds for 5 million) before keeping its 1,000 log sample. It now reads only the newest logs it samples, one project at a time: about 50 ms and 0.3 seconds. - Trace ingestion writes every trace summary of a request at once (#309):
POST /v1/otlp/traceswrote one trace summary per trace of the request, one awaited database round trip after the other. A request with spans of 500 traces took 206 seconds on ClickHouse (every summary waited for the async insert flush), 0.4 seconds on TimescaleDB and 0.5 seconds on MongoDB; it now takes 0.3, 0.08 and 0.1 seconds, with one multi-row upsert on TimescaleDB, onebulkWriteon MongoDB and one insert on ClickHouse. Collectors exporting large batches to ClickHouse no longer time out and retry.@logtide/reservoirgainsupsertTraces(traces);upsertTracestays and does the same for one trace. Note thatRATE_LIMIT_MAX(1,000 requests per minute per API key by default) also applies toPOST /v1/otlp/traces: exporters that send one span per request (a simple span processor instead of the batch processor) are capped at about 16 spans per second.
Fixed
- Project health emails for projects that do not send all the time: a project that sleeps at night, between batch runs or over the weekend opened a new "stopped sending data" issue for every pause, and every one of them sent an opened and a resolved notification (email, webhook, PagerDuty and in-app). A project that sends nothing for a while is not necessarily broken, so the "stopped sending data" check is now opt-in per project: turn it on in Project settings > Notifications ("Alert when this project stops sending data") for projects that should send data all the time, and it is a warning instead of critical. To know whether a service is up, use an uptime monitor (a heartbeat your app pings, or a log heartbeat on one service). On upgrade, projects whose silent threshold was changed from the default 60 minutes keep the check on (migration 077); on every other project it is off, and an open "stopped sending data" issue is resolved by the next health pass without a notification. The project page now shows a quiet project as "No recent data" instead of a passing "Receiving data". On top of that, any health check that opens again within 24 hours of its previous issue resolving is a recurrence: it is shown on the project page as usual but notified neither when it opens nor when it resolves, and a check that stayed quiet for more than 24 hours notifies again the next time it opens. The settings API gains
silentEnabled(PUT /api/v1/projects/:id/notification-settingsand thesettingsofGET /api/v1/projects/:id/health). - Newest data missing from TimescaleDB charts and counters: the hourly and daily continuous aggregates were materialized-only and refreshed once an hour (hourly) or once a day (daily), one bucket behind. The metrics overview and the metric charts with a 1 hour or 1 day interval read only those aggregates, so metrics sent in the last one to three hours (up to two days for daily charts) did not show up there. The dashboard charts and counters read the aggregate before a cutoff (one hour or one day ago) and raw data after it, so the hours between the last refresh and the cutoff were missing from "Total logs today", the 24 hour log and detection charts and the top services (up to two hours, or most of a day for the top services). Migration 076 turns on real-time aggregation for every continuous aggregate (the rows not materialized yet are computed from the raw tables at query time) and refreshes them every 10 minutes (hourly) or every hour (daily), so that part stays small. The dashboard and the log search split aggregate and raw data at the start of the current UTC hour or day, so nothing is read twice. On a large install the first refresh after the upgrade materializes up to three days of data once in the background. After the upgrade these numbers go up on TimescaleDB installs: they now include the hours that were missing, so they are not comparable with what 1.6.0 showed.
- Incidents created by hand or from grouped detections had no "created" entry in their history: the incident timeline started with the first change instead of the creation. It now records who created the incident (no user for incidents grouped by the system). Incidents created before the upgrade keep their history as it is.
- "Add facet" was unreadable with many metadata fields: with more fields than the menu could fit, the rows shrank instead of scrolling and their text was cut in half. The rows keep their height and the list scrolls, and a filter at the top of the menu finds a field among hundreds.
- Client IP behind a reverse proxy: with the default
TRUST_PROXY=false, every request that reached the backend through a proxy carried the proxy address (for example the Docker gateway172.31.0.1), so the audit log showed the same IP for everyone and all clients shared the login, registration and password reset rate limits.TRUST_PROXYnow also accepts the number of proxies in front of the backend (1for one reverse proxy,2for Cloudflare in front of it) or a comma-separated list of proxy IPs and CIDRs; the client IP is then the address right before those proxies, so a client cannot fake it with its ownX-Forwarded-For.truestill trusts every hop, as before, and takes the left-mostX-Forwarded-Forentry, which the client controls. An invalid value stops the server at start with an error. Set it in your.envif LogTide runs behind a proxy (see.env.example). - Dropdowns outside the design system: 47 dropdowns (metric aggregate/interval/service, trace and session filters, live tail size, monitor and status page forms, alert rule scope and renotify, SIEM filters, audit and delivery logs, PII masking scope, admin filters, invite role and others) were plain browser
<select>elements, so they opened the operating system menu instead of the app's own, ignoring the theme and dark mode. They now use the same Select component as the rest of the app, with the same keyboard navigation. - Traefik overlay did not start:
docker/docker-compose.traefik.ymlfailed withcontainer_name '' does not match patternon current Docker Compose versions, and past that Traefik 3.2 cannot talk to Docker Engine 29 (which dropped the older API versions), so it found no service and answered 404 to everything. Itsports: []also left the backend and frontend published on their own ports next to Traefik, and Traefik was not on the network of the services it routes to. The overlay now uses!reset(Docker Compose 2.24 or newer), Traefik 3.6 and the LogTide network, and setsTRUST_PROXY: "1"instead of"true", so the client IP is the one Traefik saw rather than whatever the client put in its ownX-Forwarded-For. Use2there when Cloudflare or another proxy sits in front of Traefik. Checked with two backends and two workers behind it. - Alert rule errors showed only the status code: saving an alert rule the server rejected showed "HTTP 400" instead of the reason (the alerts client read
message, the server sendserror). - PostgreSQL shared memory in the Docker Compose files: Docker gives containers 64 MB of
/dev/shm, which is too little for parallel queries andVACUUMon large TimescaleDB tables (could not resize shared memory segment ... No space left on device), so they failed or ran without parallel workers. Thepostgresservice of every compose file now setsshm_size: 1gb(an upper bound, not memory reserved up front). Existing installs pick it up when the container is recreated (docker compose up -d). - ClickHouse trace summaries were wrong with the async buffer (#309): the ClickHouse engine recomputed a trace's span count, time range and error flag from the spans table, but with
RESERVOIR_BUFFER_ENABLED=truethe spans of the request are usually still in the buffer at that point, so traces showed too few spans and a too short duration. The summary no longer reads the spans table on any engine: each request writes the summary of the spans it carried and the engine merges them, so the result does not depend on when the spans land. - Late spans cleared the root operation of a trace on MongoDB and ClickHouse: a request that did not carry the root span (for example a child span exported after the root) overwrote the trace's root service and operation with empty values, so the trace list showed no operation for it. They are now kept once known, as TimescaleDB already did. Traces affected before the upgrade keep their missing values.
- BullMQ workers now apply job retention too: BullMQ falls back to the worker's
removeOnComplete/removeOnFailfor jobs that carry none of their own, and the workers had none, so such a job was kept in Redis forever. Workers now use the same retention as the queues (keep 100 completed jobs for 1 hour, 50 failed jobs for 24 hours). Jobs added through LogTide already carried this since 1.0.3; this covers the remaining paths.
Changed
- ClickHouse traces table migration (runs once on start): on ClickHouse the
tracestable moves fromReplacingMergeTreetoAggregatingMergeTree, which merges the partial summaries of a trace. The first start of 1.6.1 copies the existing table into the new layout, checks that the trace and span totals match, swaps the tables and keeps the previous one astraces_backup_<timestamp>; drop it once you are happy with the upgrade. In our tests 20 million traces took about 10 seconds and 300 MiB of ClickHouse memory; the copy needs free disk for a second copy of the table. TimescaleDB and MongoDB need no migration. Before upgrading a ClickHouse install, stop every backend and worker of the older version: they keep writing to the old table, which makes the check fail and the migration stop without changing anything until they are gone. When the backend and the worker start together, one of them migrates and the others wait for it (at most 30 minutes). If the process that migrates dies, the next start finishes a swap that was already under way; if it died during the copy, the others refuse to start with an error naming it instead of copying the table twice. The traces table is unchanged in that case: check that no LogTide process is still running the migration, runDROP TABLE traces_migration_lockand start again. To go back to 1.6.0, runRENAME TABLE traces TO traces_1_6_1, traces_backup_<timestamp> TO traceswith LogTide stopped; traces written after the upgrade are then only intraces_1_6_1. - Docker Compose files: the backend and worker environment, the same in both, is now one shared
x-backend-envblock, so the two cannot drift apart. Both services get the database and Redis as separate settings (see Added) instead of aDATABASE_URLbuilt in the compose file, and theredisservice gets its password through the environment instead of its command line, so a Redis password with spaces,#,;or&works too. Nothing changes in.env. If you edited the compose file to point at an external database, setDATABASE_URL(orDB_HOST) in thex-backend-envblock.install.shno longer writes aDATABASE_URLline, which the compose file never read.