v1.3.0
Released
Feature
1.3.0: Geo Map & Log Table Panels, Discord Webhooks
A feature release. Custom dashboards gain a GeoIP world map and a raw log table with configurable metadata columns, webhook channels pointing at Discord finally work, and repeated webhook events no longer get stuck at pending forever.
- Geo map panel: plot GeoIP-enriched log volume on a world map, per country or per location, with no tile servers and no CDN assets
- Log table panel (#289): raw log rows on a dashboard with configurable metadata columns and an optional live tail
- Webhook channels pointing at Discord now work: every event is delivered as a Discord embed instead of a rejected envelope
- Repeated webhook events no longer get stuck at pending forever: deduplication is now explicit and time-bounded
- The graphile-worker queue pool no longer crashes the backend when the database restarts (#291)
- Eight Dependabot advisories cleared, including a SvelteKit Accept-header ReDoS reachable on the public frontend port
A feature release. Custom dashboards gain two new panel types — a geo map that plots GeoIP-enriched log volume on a world map and a log table that shows raw log rows with the metadata columns you choose — and the webhook dispatcher gets two long-standing fixes: Discord destinations now receive a payload Discord accepts, and repeated events no longer disappear into a permanent pending state. Plus a queue-pool crash fix for PostgreSQL-backed deployments and eight dependency security bumps.
Added
- Geo map panel for custom dashboards: a new
geo_mappanel plots GeoIP-enriched log volume on a world map, in two modes — aggregate bubbles per country (centroid-placed) or bubbles per rounded location (~1 km precision) with city-level tooltips. To make this aggregatable on all three storage engines, thegeoippipeline step now writes flat metadata keys alongside its existing nested object (<target>_country,<target>_country_code,<target>_city, and<target>_place, which packs rounded coordinates plus a display label into a single value so the map is one group-by, never a raw-row scan). Existing nested output is unchanged; logs ingested before this release simply do not appear on the map. The map renders with Leaflet over a bundled Natural Earth world outline — no tile servers, no CDN assets, so it works air-gapped and follows the app theme — bubbles scale with the square root of the count, and geo values that fail defensive parsing (this metadata is client-written and untrusted) are dropped and surfaced as a skipped-values note on the panel. Filters: time window, log levels, service, hostname; the geo field prefix is configurable for pipelines with multiple GeoIP steps - Log table panel for custom dashboards (#289): a new
log_tablepanel shows individual log rows (newest first) with user-configured metadata columns, resolved with the same semantics as Log Search custom columns (exact key first, then dot path into nested objects, e.g.http_hostorgeo.city) — so access-log style hit lists (host, client IP, status, GeoIP city) can live directly on a dashboard. Because the panel reads raw rows instead of aggregating, nested paths work on all three storage engines. Built-in time/level/service/message columns are individually toggleable, rows are capped (10–100) and filtered by project, service and levels, and cells are single-line with a full-value tooltip by default with an optional wrap mode. Clicking a row opens that log’s context in Search. Two refresh modes: snapshot, which follows the dashboard’s auto-refresh, or live, which tails the project’s WebSocket log stream (stream-ticket auth, so the session token never appears in the URL) and filters client-side. Requested by @SemoTech
Fixed
- Webhook channels pointing at Discord now work: Discord’s webhook endpoint accepts only bodies carrying
content,embedsor file parts, so it rejected the LogTide event envelope with HTTP 400 — the “Test channel” button reportedHTTP 400 Bad Requestand queued events went straight to the dead-letter queue after a single non-retryable attempt. Outbound bodies are now formatted per destination in the one dispatch primitive every webhook passes through, so a Discord URL receives one embed per event (title, description, severity color with green for monitor recoveries, per-type fields, organization footer, event link) from every source: channel test, monitors, alerts, incidents and errors, plus retries and replays. Detection coversdiscord.comanddiscordapp.comincludingptb./canary.subdomains and versioned API paths, and deliberately skips the/slackand/githubcompatibility endpoints, which expect their own payload shape. Existing channels need no reconfiguration, the channel form flags a Discord URL as you type it, and the delivery log keeps storing the canonical envelope so replays and the delivery detail view still show the event - Repeated webhook events no longer get stuck at
pendingforever: the dispatcher deduplicated double-enqueues through the queue job key derived from organization, event type and event id. On the Redis/BullMQ backend a job id stays known after the job completes unless the job is set to be removed, so that key deduplicated not just concurrent enqueues but every future occurrence of the same event. Callers that reuse an event id across occurrences (monitor status changes key on monitor, status and URL) therefore delivered only the first ever occurrence: every later one still created a delivery row whose job the queue silently dropped, leaving it visible in the delivery panel atpendingwith0/5attempts forever. The PostgreSQL/graphile backend frees the key when the job ends, so the same code behaved differently per backend. Deduplication is now explicit and bounded in the dispatcher: an identical event still in flight and created withinWEBHOOK_DEDUP_WINDOW_MS(default 60s) reuses that delivery, anything older or already terminal starts a new one, and queue job keys are now per delivery row so the queue can only collapse two workers racing the same row. All webhook jobs now clear themselves on completion, which also makes replaying the same delivery more than once work. Delivery rows already stuck atpendingstay unprocessable and can be deleted; no new ones accumulate - The graphile-worker queue pool no longer crashes the backend on connection loss (#291): the dedicated connection pool created for the PostgreSQL queue backend registered no error handler, so when the database terminated a pooled connection (routine restart, maintenance, host reboot), the pool’s unhandled error event took down the whole backend or worker process with an uncaught exception. This only affected deployments running the queue on PostgreSQL (no
REDIS_URL); the main database pool and the notification listener already handled this correctly, the queue pool was the one gap. The pool now logs the error and lets the driver replace the dead connection. Reported by @Erasure5959 with a full production stack trace and root-cause analysis - Dialogs taller than the viewport no longer hide their own buttons: the dialog primitive is fixed and vertically centred with no height bound, so a dialog whose content exceeded the window spilled off both edges at once — title upwards, action buttons downwards — with no way to scroll to either, since page scrolling does not reach a fixed element. On a 1366x768 laptop viewport the add-panel dialog rendered 938px tall with its primary button below the fold, and the panel config dialog for the new log table panel rendered 1228px tall with “Save” completely out of reach. Dialogs now cap at the viewport height and scroll as a safety net app-wide, and the two dashboard dialogs that grow with content scroll their own body so headers and footer actions stay pinned
- Settings → Organization no longer shows a stale retention value (#288): the organization object cached in the browser (kept to avoid a name flash on reload) was never re-fetched once present, so a retention change made in Admin never reached the member-facing Settings page within a persisted session, and hard refreshes did not help. The dashboard now revalidates the organization list in the background whenever a cached organization exists, and the retention card no longer coerces a missing value to “90 days”, which made any stale or pre-retention cached object look like a real 90-day policy. Reported by @SemoTech with an accurate frontend root-cause analysis
- Syslog collection requirements documented (discussion #292): nothing stated that the bundled Fluent Bit container — the only syslog listener (port 514 UDP/TCP, RFC 3164) — sits behind the
loggingcompose profile and therefore does not run in a default installation, whileinstall.shstarts the stack without profiles. Users pointing network devices at port 514 got silence with no hint why. The README profile list, both example env files and the installer’s closing summary now spell out the two-step enablement (setFLUENT_BIT_API_KEY, start with--profile logging), andSYSLOG_TZ_OFFSETis documented too, since RFC 3164 timestamps carry no timezone and devices sending local time otherwise land outside the UI’s default 24h window FLUENT_BIT_API_KEYdocumented in the example env files: the bundled Fluent Bit services pass this variable to the collector as its API key, but it appeared in neither example env file, so a user following the examples ended up with Fluent Bit sending an empty key. Every ingest request was then rejected with 401 while the whole stack looked healthy, which reads as “LogTide is running but receives no logs”
Security
- Bumped
@sveltejs/kitto 2.70.2 (GHSA-29g2-3rmr-qm68, moderate, plusGHSA-866w-xmhq-wj7xandGHSA-wqjv-9729-c5q2): versions up to 2.70.1 parse theAcceptheader for content negotiation with a quadratic regular expression, so a single crafted header pins a CPU core. The frontend image runs the SvelteKit Node server directly, so this was reachable unauthenticated on the public frontend port — the one advisory in this batch that mattered in practice. The other two cover remote-function form handling, which LogTide does not use - Bumped
js-yamlto 4.3.x (GHSA-52cp-r559-cp3m, high): affected versions expand chained YAML merge keys in quadratic time, so a small document can consume minutes of CPU.js-yamlparses caller-supplied documents in three import endpoints (Sigma rules, custom dashboards, log pipelines), so an authenticated user could stall a worker with a short payload - Bumped
undicito 7.29.0 (GHSA-4cwx-7wf7-3272, high, plus four related advisories): a set of response-parsing and interceptor issues (cross-user disclosure via degenerate cache directives, CRLF injection through a blob body type, cookie attribute injection, retry-interceptor response desynchronization). LogTide usesundiciin exactly one place, the SSRF guard’s outbound fetch, with a plain agent and no cache or retry interceptor, so none of the five were reachable as configured - Bumped
find-my-wayto 9.7.0 (GHSA-c96f-x56v-gq3h, high): Fastify’s router can be driven into a denial of service over HTTP/2. LogTide serves HTTP/1.1 everywhere, so this was not reachable, but the router is on the request path for every API call and the floor is worth holding - Bumped
fast-urito 3.1.5 (three high advisories): host confusion when parsing authorities that use a backslash introducer or that fail IDN canonicalization. It reaches LogTide only through schema validation of a Sigma rule’s webhook URL, and nothing security-relevant is decided there: outbound requests are gated by the SSRF guard, which re-parses the URL, resolves it and revalidates every redirect hop - Bumped
postcssto 8.5.26 (GHSA-r28c-9q8g-f849,GHSA-6g55-p6wh-862q, both high): an attacker-controlled source-map comment in CSS makes the previous-source-map loader read an arbitrary file off disk and fold it into the build output. PostCSS runs only at build time here, over first-party stylesheets - Bumped
brace-expansionto 5.0.9 (GHSA-rgw5-rvv9-x895,GHSA-mh99-v99m-4gvg, high): the first is an explicit bypass of theCVE-2026-13149mitigation pinned for in 1.2.0, the second an out-of-memory crash from unbounded expansion length. Pulled in transitively through tooling, so not on any request path - Bumped
shell-quoteto 1.10.0 (GHSA-395f-4hp3-45gv, high): its parser is quadratic in the input length. Used only by development tooling and never invoked on user input