Scheduled digest reports, webhook event receivers, full documentation coverage, migration guides from ELK/Datadog/Loki, and production-ready Helm chart.
Scheduled digest reports via email
Webhook event receivers
Full documentation coverage
Migration guides for ELK, Datadog, Loki
Production-ready Helm chart
Planned for v1.0 Beta
Scheduled Digest Reports — Email-based summaries on a configurable schedule
Webhook Event Receivers — Accept events from external services and integrations
Full Documentation — Complete documentation coverage for all features
Migration Guides — Step-by-step guides for migrating from ELK, Datadog, and Grafana Loki
Helm Chart Stable — Production-ready Kubernetes deployment via Helm
v1.x
Planned
Feature
H2 2026
Stable Release & Enterprise Features
Performance hardening, enterprise SSO/SAML, advanced RBAC, managed cloud GA, and tiered storage with hot/warm/cold data lifecycle.
Performance hardening for production scale
Enterprise SSO/SAML
Advanced RBAC
Managed cloud GA
Tiered storage (hot/warm/cold)
Planned
Performance Hardening — Production-scale optimization and stress testing
Enterprise SSO/SAML — SAML 2.0 integration for enterprise identity providers
Advanced RBAC — Fine-grained role-based access control
Managed Cloud GA — General availability of the hosted LogTide cloud service
Tiered Storage — Hot/warm/cold data lifecycle management for cost optimization
2026
v0.9.7
Released
Fix
SSRF Fix in Alert/Sigma Webhook Delivery
Closes a sibling-gap left by the 0.9.6 SSRF hardening: the alert/Sigma webhook delivery path now routes through the centralized safeFetch guard instead of the bypassable inline filter (GHSA-7v53-pw6r-99vj, CWE-918).
SSRF fix: alert/Sigma webhook delivery now uses the centralized safeFetch guard (GHSA-7v53-pw6r-99vj, CWE-918)
DNS resolution + per-redirect-hop revalidation + full IPv4/IPv6 private/reserved range coverage on webhook delivery
Read-back oracle closed: blocked targets are rejected before the response body is read
MONITOR_ALLOW_PRIVATE_TARGETS still lets self-hosted deployments target internal endpoints
Security
SSRF in alert/Sigma webhook delivery via the legacy delivery path (authenticated) (GHSA-7v53-pw6r-99vj, CWE-918): the 0.9.6 SSRF hardening added the centralized utils/ssrf-guard.ts guard and wired it into the HTTP/TCP monitors and the WebhookProvider, but the actual alert/Sigma webhook delivery path was left on the old inline filter. sendWebhookNotification in queue/jobs/alert-notification.ts (reached by the alert-notifications BullMQ worker for threshold, rate-of-change and Sigma-rule alerts) still ran a bare fetch(webhook_url, …) guarded only by a string-based isPrivateIP(). That check was bypassable: any non-dotted-quad hostname returned false (no DNS resolution), so a domain whose A record points at 127.0.0.1 / 169.254.169.254 / an internal host passed the filter and the resolved internal address was then connected; the bare fetch used the default redirect: 'follow', so a public host that 302s to an internal URL was followed straight there; and CGNAT (100.64.0.0/10), IPv6, IPv4-mapped IPv6, 0.0.0.0/8 and 198.18.0.0/15 were not covered. An authenticated org owner/admin who can create a webhook notification channel could therefore use the backend as a blind SSRF probe against internal services and cloud metadata, with a partial read-back oracle since a non-2xx internal response body was spliced into the alert-history error message. The guarded “Test” button already blocked the same URLs, confirming this as an incomplete-fix sibling-gap. Fix: sendWebhookNotification now routes delivery through safeFetch(url, init, { allowPrivate: config.MONITOR_ALLOW_PRIVATE_TARGETS }), exactly as WebhookProvider does (DNS resolution + per-redirect-hop revalidation + full IPv4/IPv6 private/reserved range coverage), mapping SsrfBlockedError to the existing “private/internal addresses are not allowed” error. The inline isPrivateIP/BLOCKED_HOSTS filter and the bare fetch are removed, and blocked targets are now rejected before the response body is read (closing the read-back oracle). The MONITOR_ALLOW_PRIVATE_TARGETS opt-in still lets self-hosted deployments target internal endpoints. Reported by tonghuaroot
Frontend now runs as a full SPA on adapter-node (kills hydration bugs), SSRF guard on monitors and webhooks, cross-tenant projectId validation on four routes, OIDC iss-parameter fix for Authelia, and an infinite-skeleton fix on search/traces/metrics.
Frontend converted to a full SPA (ssr=false root cascade), removing 22 per-route ssr=false workarounds and a whole class of hydration bugs
Security: SSRF guard on uptime monitors and webhooks, denied by default (MONITOR_ALLOW_PRIVATE_TARGETS to opt in)
Security: cross-tenant projectId validation enforced on alerts/preview, alerts, monitors and sourcemaps routes
OIDC login fixed for issuer-identifying providers (RFC 9207 iss param) like Authelia (#233, #234)
Fixed infinite skeleton spinner on search/traces/metrics when no project had a data-availability flag
Changed
Frontend now runs as a full SPA on adapter-node instead of doing SSR with client hydration. A single export const ssr = false in the new root +layout.ts cascades to every route, so the server only ships the empty app shell and the client renders from scratch. Eliminates the entire class of hydration mismatch bugs that had been accumulating across login, register, invite, public status page, the /auth/callback page and the various dashboard subpages, each previously patched with its own per-route ssr = false. Server load functions and the Node runtime are untouched (the adapter, API proxying, env vars, BullMQ etc. keep working exactly as before); there are no +page.server.ts / +layout.server.ts files in the repo today so nothing had to change semantically on the server side. UX tradeoff: the public status page at /status/[orgSlug]/[projectSlug] now flashes the empty shell for one paint before the JS hydrates, which is acceptable for an authenticated-by-default product but should be revisited if SEO on the status page becomes a goal (a single-line override export const ssr = true in that page’s +page.ts puts it back on SSR without affecting anything else)
Removed 22 redundant route-level ssr = false declarations (dashboard/+layout.ts, dashboard/admin/+layout.ts and 20 +page.ts files across landing, login, register, onboarding, invite, status, auth callback and the dashboard search / metrics / alerts / monitoring / projects / sessions / settings subtrees) that had been added one by one as each page hit a hydration bug. The new root-level cascade makes them all dead config; deleting them removes the temptation to copy the pattern into new routes
Fixed
Infinite skeleton spinner on /dashboard/search, /dashboard/traces and /dashboard/metrics when no project in the org had its data-availability flag set, typically right after a user deleted the only projects that had been ingesting. The filter logic at search/+page.svelte:441-444 (and the identical pattern at traces/+page.svelte:142-145 and metrics/+page.svelte:90-93) read const logsProjectIds = availability?.logs and then branched on logsProjectIds ? filter : fallback. When getProjectDataAvailability legitimately returned { logs: [] } the empty array took the truthy branch ([] is truthy in JS) and [].includes(p.id) excluded every project, so the displayed project list was empty, loadLogs() never fired, and hasLoadedOnce stayed false so the SkeletonTable rendered forever. Fix: guard the truthy branch with logsProjectIds && logsProjectIds.length > 0 so an empty availability response falls back to “show all projects” exactly like the API-failure path (.catch(() => null)) already does. The 0.9.4 backend optimization that introduced the cached availability flags is unaffected; this is purely a frontend null-vs-empty conflation. Logs of already-hard-deleted projects on the TimescaleDB engine remain unrecoverable due to ON DELETE CASCADE on logs.project_id (tracked separately under the soft-delete projects epic)
OIDC login failed against issuer-identifying providers (e.g. Authelia) because the iss callback parameter was dropped (#233, #234): the OIDC callback handler extracted only code and state from the provider redirect and rebuilt the callback URL from just those two, discarding everything else. Providers implementing RFC 9207 (OAuth 2.0 Authorization Server Issuer Identification) append iss to the redirect and openid-client’s authorizationCodeGrant() validates it, so the token exchange failed with “issuer parameter missing” and login never completed. The Fastify callback route now forwards the full request.query through handleOidcCallback into the provider, which replays every parameter onto the callback URL handed to the token exchange (so iss, session_state, etc. survive); the required code/state are always re-asserted from the validated values. Duplicated/array-valued query params are collapsed to a single value with searchParams.set instead of being appended, since OIDC authorization-response params are single-valued per RFC 6749 / RFC 9207, avoiding a malformed URL with duplicate iss/code reaching authorizationCodeGrant. Covered by new tests across the route, service and provider layers, including the array-collapse and undefined-param branches
Security
Cross-tenant project access via unvalidated projectId (authenticated): several routes accepted both an organizationId and a projectId, verified the caller was a member of the organization, but never verified that the supplied project actually belonged to that organization. Since project UUIDs are normal identifiers that appear in dashboard URLs and client API calls, a user could pair their own organizationId (membership check passes) with a victim’s known projectId and reach another tenant’s data. Confirmed on four handlers: POST /api/v1/alerts/preview returned sampleLogs (time, service, level, message, trace ID) from the foreign project; POST /api/v1/alerts and POST /api/v1/monitors let a rule/monitor be scoped to a foreign project (the monitor case also surfaces on the victim’s public status page, which renders by project_id); and GET/DELETE /api/v1/sourcemaps let a member list or delete another tenant’s source maps. Fix: a shared projectsService.projectBelongsToOrg(projectId, organizationId) helper (single projects lookup filtered on both id and organization_id) is now enforced right after the existing membership check on each of those routes, returning 403 when the project is foreign. The alert/monitor update paths don’t accept a projectId and the custom-dashboards panel pipeline already had an equivalent ensureProjectInOrg guard at its choke point, so no change was needed there. Regression tests cover each handler
Server-side request forgery (SSRF) and internal port scanning via monitors and webhooks (authenticated): HTTP/TCP uptime monitors and webhook delivery executed user-supplied targets from the backend’s network with no meaningful destination validation. Monitor creation only checked that an HTTP target started with http(s):// and that a TCP target contained :; checker.ts then called fetch(target, { redirect: 'follow' }) and createConnection({ host, port }), so a registered user could point a monitor at http://169.254.169.254/…, http://127.0.0.1, 10.0.0.0/8, etc. and use the sanitized up/down result and timing to probe internal services. The webhook provider had only literal-string private-IP filtering (no DNS resolution, incomplete IPv6, followed redirects), leaving DNS- and redirect-based bypasses open. Fix: a centralized outbound guard (utils/ssrf-guard.ts) resolves hostnames and rejects loopback, private, link-local (incl. 169.254.169.254 cloud metadata), CGNAT (100.64.0.0/10), multicast and reserved IPv4/IPv6 ranges (including IPv4-mapped IPv6 and ULA/fc00::/7, link-local fe80::/10). TCP checks resolve-then-pin the socket to the validated address (closing DNS-rebinding between validation and connect); HTTP checks and webhook delivery follow redirects manually and revalidate every hop instead of redirect: 'follow'. The guard runs both at monitor create/update time (immediate 400 feedback) and at execution time (authoritative, returns a blocked result). Private/internal targets are denied by default; self-hosted deployments that legitimately monitor internal services can opt back in with MONITOR_ALLOW_PRIVATE_TARGETS=true, which also governs webhook delivery. Note: HTTPS does not yet pin the connected address against a custom dispatcher, so a narrow DNS-rebinding window remains for HTTP monitors (tracked for a follow-up); the reported direct-target and redirect-to-internal vectors are closed
Metadata filters now work on ClickHouse and MongoDB, error notifications are throttled per group to stop email storms, the notification-channels defaults endpoint accepts monitoring, plus 19 Dependabot advisories resolved.
Metadata filters now translated on ClickHouse and MongoDB, not just TimescaleDB (#226, #224)
Error notifications throttled per error group via a race-safe cooldown (ERROR_NOTIFICATION_COOLDOWN_MINUTES, default 15)
notification-channels defaults endpoint now accepts the monitoring event type
Migration 043 adds last_notified_at to error_groups
Metadata filters were silently ignored on ClickHouse and MongoDB (#226, issue #224): the metadata filter operators (equals, not_equals, in, not_in, exists, not_exists, contains) were only translated by the TimescaleDB query builder. The ClickHouse and MongoDB query translators never read params.metadataFilters, so any log search or alert rule that relied on a metadata.* filter came back unfiltered on those engines (the filter appeared to do nothing). ClickHouse now translates each filter to a predicate over the JSON metadata column using JSONExtractString paired with JSONHas to distinguish a missing key from an empty string (not_in/not_equals split on whether the key is present, contains uses positionCaseInsensitive). MongoDB builds one clause per filter keyed on metadata.<key>, all wrapped in $and so repeated filters on the same key don’t overwrite each other, with include_missing controlling whether not_equals/not_in also match documents where the field is absent ($exists). Covered by new per-engine translator tests
Error notifications spammed one email per occurrence: processErrorNotification sent an in-app notification, email and webhook for every exception occurrence, suppressed only when the error group’s status was ignored. A high-frequency client error (e.g. a Svelte effect_update_depth_exceeded loop firing thousands of times) produced one exception row per occurrence and therefore thousands of identical alert emails. The job now throttles per error group: it atomically claims a notification slot via UPDATE error_groups SET last_notified_at = now() WHERE status != 'ignored' AND (last_notified_at IS NULL OR last_notified_at <= cutoff) RETURNING id, so only the first occurrence inside the cooldown window notifies and the rest are skipped. The conditional UPDATE is race-safe (concurrent jobs serialize on the row lock and re-evaluate the predicate against the freshly written timestamp), so even a burst of thousands collapses to a single notification per window. Cooldown is configurable via ERROR_NOTIFICATION_COOLDOWN_MINUTES (default 15, set 0 to notify on every occurrence). Occurrence counts on the error group and in-app dashboards are unaffected
monitoring rejected by the notification-channels defaults endpoint: PUT/GET /api/v1/notification-channels/defaults/:eventType validated :eventType against a local Zod enum of ['alert', 'sigma', 'incident', 'error'] that was missing monitoring, even though the shared NotificationEventType type, the DB organization_default_channels constraint (migration 037) and the service all support it. Setting a default monitoring channel returned 400 Validation error (“received ‘monitoring’”). Added monitoring to the route enum so the five event types are consistent across the stack
Added
Migration 043 error_notification_throttle: adds a nullable last_notified_at TIMESTAMPTZ column to error_groups, used as the per-group notification cooldown anchor (see the error-notification throttle fix above)
Security
Resolved 19 Dependabot advisories (8 high, 11 moderate) by bumping direct dependencies and pinning patched versions through the root pnpm overrides. protobufjs → 7.6.1 (kept on the 7.x line via >=7.5.8 <8; covers code injection, prototype-pollution gadget, unbounded-recursion DoS, unsafe option paths, crafted-field DoS, overlong UTF-8 in @protobufjs/utf8 >=1.1.1). kysely → 0.28.17 (bounded >=0.28.17 <0.29; JSON-path traversal injection in JSONPathBuilder.key()/.at()). svelte → 5.55.9 (>=5.55.7; SSR XSS via spread attributes and promise serialization, DOM-clobbering XSS, <svelte:element> ReDoS). @sveltejs/kit → 2.61.1 (>=2.60.1; query.batch cross-talk). fast-uri → 3.1.2 (path traversal + host confusion via percent-encoded segments). qs → 6.15.2 (DoS in qs.stringify), devalue → 5.8.1 (sparse-array DoS), brace-expansion → 5.0.6 (numeric-range DoS), ws → 8.21.0 (uninitialized memory disclosure). protobufjs and kysely were deliberately held on their current major/minor lines (their “latest” is a breaking jump) while still landing on the patched release
Data-availability endpoint now reads cached flags on projects (6s+ → <50ms on ClickHouse), activity_overview panel works across all engines, 14 dashboard pages get proper titles, plus uuid 14 and fast-xml-parser 5.7.3 security bumps.
GET /api/v1/projects/data-availability rewritten to read cached has_X_at flags — 6s+ → <50ms on production ClickHouse
Boot-time one-shot backfill for the new project flags, throttled and gated by system_settings
activity_overview panel logs/log_errors series now work on ClickHouse and MongoDB via reservoir.aggregate
14 dashboard pages now set proper <title> instead of falling back to generic 'LogTide'
Security: uuid 14.0.0 (CVE-2026-41907) and fast-xml-parser 5.7.3 (CVE-2026-41650) bumps
Changed
GET /api/v1/projects/data-availability rewritten to read from cached flags on projects instead of fanning out N*3 existence queries to the reservoir per call. In production (ClickHouse, ~70M rows) the old path scanned count(*) and LIMIT 1 probes across logs/traces/metrics for every project in the org, taking 6s+ on non-trivial orgs. The new path is a single SELECT id, has_logs_at, has_traces_at, has_metrics_at FROM projects WHERE organization_id = $1 plus an in-memory staleness filter against organizations.retention_days, returning in <50ms regardless of ingest volume. The response shape is unchanged ({ logs: string[], traces: string[], metrics: string[] })
Ingest paths mark data-availability flags fire-and-forget: after a successful batch in logs / traces / metrics ingest, projectsService.markHasData(projectId, kind) updates projects.has_X_at. A module-scoped in-memory debounce (5 min per (projectId, kind)) prevents UPDATE spam on hot projects: at most one UPDATE per 5 min per pod per project per kind. Failures are logged and swallowed so ingest is never impacted; the boot-time backfill is the safety net
Added
Migration 042 project_data_availability: adds nullable has_logs_at, has_traces_at, has_metrics_atTIMESTAMPTZ columns to projects. No backfill in SQL (handled at runtime, see below)
One-shot backfill at server boot: runDataAvailabilityBackfill() runs fire-and-forget after app.listen, guarded by a data_availability.backfilled row in system_settings so subsequent boots are a no-op. It selects every project with all three flags still NULL and, for each, runs three LIMIT 1 probes (logs / traces / metrics) via the reservoir in parallel. Throttled in batches of 10 with a 50ms pause to avoid hammering ClickHouse/Timescale/Mongo at boot. On a deployment with 1000 projects this takes ~6s on ClickHouse or MongoDB, up to ~30s on TimescaleDB when most projects are empty (chunk scan confirming zero rows). Force a re-backfill by deleting the safety row
Multi-engine staleness rule: a project is reported as “has data” only if has_X_at >= now() - organizations.retention_days. Handles the “data aged out by retention” case without needing a background worker. The same logic works identically on TimescaleDB, ClickHouse and MongoDB deployments because the flag lives in Postgres and the reservoir only backs the ingest and backfill paths
Fixed
activity_overview dashboard panel showed empty logs / log_errors series on ClickHouse and MongoDB: the fetcher in panel-data-service.ts gated both the logs and the spans queries behind reservoir.getEngineType() === 'timescale', so on non-Timescale engines those branches were skipped entirely and the panel rendered with all four log/span counters flat at zero (only detections and alerts, which read from Postgres operational tables, kept working). The logs path now branches: TimescaleDB still uses the logs_hourly_stats / logs_daily_stats continuous aggregate with the raw logs fallback, while ClickHouse and MongoDB go through reservoir.aggregate({ interval: '1h' | '1d' }) (same dual pattern already used by baseline-calculator). Bucket boundaries from toStartOfHour / $dateTrunc align with the panel’s UTC buildBucketTimes, so the keys collide cleanly. logs sums total per bucket, log_errors sums byLevel.error + byLevel.critical. Spans / span_errors stay TimescaleDB-only because IReservoir has no aggregateSpans() primitive, matching the existing convention in trace_latency and trace_volume
Missing <title> on 14 dashboard pages: every tab opened on /dashboard, /dashboard/admin/{organizations,projects,users,settings} (list and detail), /dashboard/monitoring (list and detail) and /dashboard/projects/[id]/{alerts,overview,performance,settings} showed the generic fallback “LogTide” from app.html because no <svelte:head><title>...</title></svelte:head> was set on the page. They now follow the existing Title - LogTide convention used by the rest of the app, with dynamic titles where the entity is already in scope: {$activeDashboard.name} for the custom dashboards root, {org.name} / {project.name} / {user.name ?? user.email} / {monitor.name} for the four [id] pages (each with a sensible fallback while the loader resolves). The two pure-redirect pages (/dashboard/projects/[id] and /dashboard/settings, which goto their default sub-route on mount) intentionally keep no title since the destination sets one immediately
Security
Bump uuid to 14.0.0 (GHSA-w5hq-g745-h8pq, CVE-2026-41907, MODERATE): v3, v5 and v6 accepted a caller-provided buf + offset but did not validate bounds, so a small buffer or an out-of-range offset produced silent partial writes instead of throwing RangeError like v1/v4/v7 do. uuid is pulled in transitively by bullmq and ldapts (production) and bson/ioredis (dev only); both production consumers resolve to their ESM entry points from our "type": "module" backend, so the major bump from ^11.1.0 is safe (uuid 12+ dropped CommonJS, but we never load the CJS path). Added "uuid": ">=14.0.0" to the root pnpm overrides; resolves to 14.0.0. Engines requirement of Node 20+ is already satisfied by engines.node: ">=20.0.0"
Bump fast-xml-parser to 5.7.3 (GHSA-gh4j-gqv2-49f6, CVE-2026-41650, MODERATE): XMLBuilder did not escape the --> sequence in comment bodies or the ]]> sequence in CDATA sections, so user-controlled data flowing into either could break out and inject sibling XML nodes (XSS in browser-rendered XML, SOAP injection, RSS/SVG payloads, etc.). The fix lands in 5.7.0; we use it transitively only via @aws-sdk/xml-builder pulled in by @types/nodemailer (dev), but bumping the override from >=5.5.7 to >=5.7.0 clears the lockfile advisory. Resolves to 5.7.3
v0.9.3
Released
Feature
Traces Live Tail, Dashboard Panels & Async Ingestion Buffer
Traces gain live tail (SSE), row expand, keyboard shortcuts and export; monitoring reorganized into tabs; user settings promoted to a full page; new trace_volume and activity_overview dashboard panels; opt-in ReservoirBuffered async ingestion with in-memory and Redis Streams transports.
Traces live tail via SSE, inline span expand, j/k/enter/r keyboard shortcuts and JSON/CSV export
Monitoring page split into Monitors / Incidents / Maintenance / Status page tabs with summary cards
User settings promoted from dialog to full page at /dashboard/account
New trace_volume and activity_overview dashboard panels using continuous aggregates with raw fallback
Opt-in ReservoirBuffered async ingestion: in-memory (signal-wakeup) and Redis Streams transports with DLQ, circuit breaker and Prometheus metrics
Added
Traces live tail (SSE): new GET /api/v1/traces/stream Server-Sent Events endpoint polls the traces table once a second and emits new trace_ids as they appear, filtered by projectId, service (CSV) and error. On the frontend the traces page now has a “Live tail” switch + rows-limit selector (50/100/200/500/1000, persisted in localStorage) in the filter bar; incoming traces are prepended and the list is capped at the chosen limit. The tracesEventSource is torn down on onDestroy and on every filter-key change
Traces row click-to-expand with inline span list: clicking a trace row now toggles an inline panel below it that fetches GET /api/v1/traces/:traceId/spans once (cached per trace_id for the session) and shows a compact table with Service / Operation / Kind / Duration / Status for each span. The “View” action still opens the full trace detail page
Traces keyboard shortcuts: / focuses the Trace ID input, r refreshes, j/k move the selection down/up with scroll-into-view, enter expands/collapses the selected trace. Registered via shortcutsStore under scope traces and unregistered on destroy
Traces export (JSON / CSV): new Export popover in the filter bar produces a client-side download of the current list (disabled during live tail or when the list is empty). CSV covers start_time / service / operation / duration_ms / span_count / error / trace_id
Monitoring entry in the command palette: cmd+k now lists Monitoring under Navigation (icon + g o shortcut hint), matching the sidebar’s Detect group so the page is reachable without leaving the keyboard
trace_volume dashboard panel: new panel type that plots span count over time (optional errors line), configurable time range (1h/6h/24h/7d) and optional service filter. Mirrors the shape of the existing logs “Log Volume” panel but for OTLP traces. Reads the spans_hourly_stats / spans_daily_stats continuous aggregate first, falling back to the raw spans hypertable with date_trunc only when the cagg is empty for the window (covers the end_offset=1h refresh lag on freshly-ingested data). TimescaleDB-only
activity_overview dashboard panel: unified multi-series timeline combining logs, log errors, spans, span errors, detection events and alert triggers on a common bucket grid (hourly for ≤24h, daily for 7d/30d). Each series is individually toggleable in the config form. Per-source queries run in parallel and prefer the logs_*_stats, spans_*_stats, detection_events_*_stats continuous aggregates for cost; if a cagg returns zero rows for the window the fetcher falls back to the raw hypertable (logs, spans, detection_events) for that source only, so the panel stays correct on freshly-ingested data without running a raw scan in the hot path. Alerts always come from alert_history + alert_rules (no cagg exists for it)
Changed
Monitoring page reorganized into tabs: what was a single 1000-line scroll with three unrelated sections (Monitors, Incidents, Maintenance) plus a status-page config block wedged at the top is now a Tabs.Root with four tabs — Monitors, Incidents, Maintenance, Status page — each with its own header, its own primary CTA, and its own empty state. The Monitors tab gains a summary card grid (Total / Up / Down / Paused, each card clickable to filter the list by that status), a search input that matches name / target / type, and a “Clear filters” shortcut. Every row now has a one-click Pause/Resume button (previously buried in the edit form). Status-page visibility, password, slug and embed-badge config moved into the Status page tab. No changes to underlying API calls or permissions
Admin dashboard sidebar collapses on mobile: the 256px admin sidebar was previously always visible, taking up a huge chunk of the viewport on phones. It now hides below lg: and opens as a fixed drawer triggered by a hamburger in a new mobile-only sticky header that also shows the current section name (Dashboard / Users / Organizations / etc). The drawer has a backdrop, is dismissed on nav-click or backdrop-click, and locks body scroll while open. Desktop behaviour unchanged
User settings promoted from a dialog to a full page at /dashboard/account. The cramped scrollable UserSettingsDialog modal (profile + password + tutorial restart + danger zone stacked inside a 32rem-wide overlay) is now a proper route using a max-w-3xl container with each concern split into its own rounded-lg border bg-card section (Profile / Change password / Onboarding / Danger zone). The dropdown menu item still says “User Settings” but now navigates instead of opening an overlay. UserSettingsDialog.svelte is removed. The delete-account confirmation remains a nested AlertDialog since that one is genuinely destructive and modal-appropriate
Public status page polished for mobile and clarity: outer container shrinks from px-4 py-10 to px-3 py-6 on phones (only sm: and up gets the roomy desktop padding). The 45-day uptime bars drop their min-w-[6px] floor to 4px on mobile so the whole row fits comfortably on 320px viewports. The footer (“Last updated …” and “Powered by LogTide”) grows from text-[10px] to text-xs with a taller top margin, and “LogTide” is now styled as text-primary font-medium hover:underline so the link affordance is obvious (previously only a hover recolor). Incident update timestamps/status labels also bumped from text-[10px] to text-xs. The monitor-type badge (HTTP/TCP/etc.) hides below sm to free up the row for the name + uptime%. The password input switches from a fixed w-64 to w-full max-w-xs so it never overflows narrow phones
Monitoring forms moved into modal dialogs: the previous inline create/edit monitor form, the “New incident” form, the “Post incident update” form and the “Schedule maintenance” form were expanding/collapsing sections that shifted the whole page layout when opened. They now open as centered Dialog modals with max-h-[90vh] overflow-y-auto so tall forms (monitor create) scroll independently of the page. The per-row “Post update” Dialog is a single instance driven by a showUpdateForm id + an updatingIncident derived lookup, replacing the one-per-row inline expand. Form state, validations and submit handlers are untouched
Traces filter UI reworked into the same two-row pill bar as the search page: row 1 keeps Time Range (TimeRangePicker in a popover), the Live tail switch, the List/Map view toggle and the Export menu. Row 2 exposes every filter as its own always-visible pill — Project (single-select), Services (multi-select), Status (tri-state: all / errors / ok), Duration range (min/max ms inputs), Trace ID (direct-navigate to the trace detail page). Pills switch to the secondary variant when a non-default value is set; “Clear all” appears when any filter is active. Mobile popovers use w-[Xpx] max-w-[90vw] so they never overflow the viewport. handleTimeRangeChange syncs picker state back into timeRangeType / customFromTime / customToTime so the trigger label survives popover close/remount
Traces backend accepts multi-value projectId and service (CSV on the query string) and two new minDurationMs / maxDurationMs bounds on GET /api/v1/traces. The cross-project access check loops over every project in the list. tracesService.listTraces widens its input types to string | string[] and forwards to reservoir.queryTraces, which already supported these filters
Search page paginator now uses the same ellipsis/windowed control as the traces page: shows up to four numbered buttons plus sentinel … on each side when totalPages > 7. Falls back to the previous “Page N” label when the backend doesn’t return a total (storage engines without total in the query response). “Previous” / “Next” labels hide on <sm screens so only the chevron shows on phones
Empty states wait for the first load to complete before rendering on the search, traces, errors and security-incidents pages. Each page now tracks a hasLoadedOnce flag flipped in the finally branch of its loader; until the first fetch resolves (success or failure) the page shows its skeleton instead of the “No Logs Yet / Start sending logs from your applications” onboarding card. Prevents the flash where an org that already has data showed the onboarding CTA during the initial render between mount and the first query. When a fetch legitimately returns zero rows, the page also distinguishes “nothing matches these filters” (with a Clear-filters shortcut) from “project has no data at all” (the full onboarding)
Search page filter UI reworked into a two-row pill bar: row 1 keeps Search + mode, a Time Range popover trigger, Live Tail and Export inline. Row 2 exposes every filter as its own always-visible pill (Projects, Services, Hostnames, Levels, Trace ID, Session ID, Metadata) — each pill displays the current value (e.g. “Service: api-gateway”, “Levels: 2”) and opens its own popover for editing. Pills switch to the secondary variant when a non-default value is set so active filters are visible at a glance. A “Clear all” link appears at the end of the row when any filter is active. Popover widths use w-[min(Xpx,calc(100vw-1rem))] so they never overflow the viewport on phones. No semantic change to the underlying filter behavior (state, query params, live-tail flow, metadata apply/clear) - purely a layout/affordance pass replacing the previous six-column “Filters” Card
Auto-created Default dashboard now uses the new activity_overview panel instead of the logs-only time_series “Log Volume” chart. Existing organizations keep their current dashboard untouched; the swap only affects orgs that don’t yet have a default (i.e. new signups or orgs where the default was deleted and re-seeded via ensureDefaultExists)
Opt-in async buffer for reservoir writes: new ReservoirBuffered decorator sits between the ingestion API and the storage engine. When enabled via RESERVOIR_BUFFER_ENABLED=true, POST /api/v1/ingest returns as soon as records are accepted into a shard-partitioned queue; a flush consumer pool drains to storage asynchronously. Two transports ship in the box: an optimized in-memory queue with signal-based wakeup (single-instance, not crash-safe) and a Redis Streams transport with consumer groups, XAUTOCLAIM-based stale reclaim, atomic MULTI/EXEC nack, and a DLQ side stream (multi-instance, durable). Circuit breaker bypasses to sync ingestion when the buffer fills beyond a configurable pending threshold or after repeated flush failures in a rolling window. Off by default; see docs/async-buffer/ for when to enable and the per-engine benchmark table
IReservoir shared interface: public type that Reservoir and ReservoirBuffered both implement, so downstream code (backend monitoring, etc.) can type against IReservoir without caring which implementation is live
Prometheus-style buffer metrics: reservoir_buffer_enqueued_total, reservoir_buffer_bypass_total, reservoir_buffer_flush_success_total, reservoir_buffer_flush_failure_total, reservoir_buffer_flush_duration_ms (histogram), reservoir_buffer_dlq_total, reservoir_buffer_breaker_state (0 closed / 1 open / 2 half-open). All labelled by record kind and shard where applicable
Warning at startup when buffer is enabled on non-Timescale engines: benchmarks show the buffer regresses p95 under saturation on ClickHouse and MongoDB (the bottleneck is the flush side, which the buffer cannot hide). The backend now logs a clear warning with a link to the docs when RESERVOIR_BUFFER_ENABLED=true is set together with STORAGE_ENGINE=clickhouse or STORAGE_ENGINE=mongodb
k6 load-test script load-tests/buffered-vs-sync.js (pnpm --filter @logtide/backend load:buffered-vs-sync): constant-arrival-rate 100 req/s for 3 min, batch size 10, reports p50/p95/p99 + error rate. The file lives in the gitignored load-tests/ directory; only the npm script is checked in
Graceful shutdown now drains the reservoir buffer in parallel with Fastify app.close(): previously serialized, so a slow app.close() (BullMQ workers, websocket connections) could exhaust the container stop timeout before shutdownReservoir() ran. The new order calls them via Promise.all, giving the buffer its full RESERVOIR_BUFFER_GRACEFUL_SHUTDOWN_MS window to drain regardless of how long app.close() takes
Redis client in the backend’s buffer path is now explicitly quit()-ed on shutdown: RedisStreamTransport documents that it does not own the client, so the backend owns the lifecycle. Without this, the Redis connection was torn down by the process exit instead of closing cleanly
Buffer start-up failure is fail-fast: when RESERVOIR_BUFFER_ENABLED=true and the consumer pool cannot start (e.g. Redis down at boot), the backend logs a CRITICAL message and process.exit(1) instead of silently continuing with a non-flushing buffer
RedisStreamTransport hardening: XPENDING now filters by consumer name in claimStale, so delivery-count mapping is accurate even on busy streams with claims from other consumers; getStats.oldestPendingAgeMs now tracks the oldest not-yet-delivered entry (via XRANGE with exclusive start past the PEL tail) to match the documented contract; nack uses MULTI/EXEC so the DLQ write and the original ACK either both land or neither does; enqueueMany inspects pipeline exec() results and throws on the first error instead of swallowing them; default consumer name changed from consumer-${pid} to ${hostname}-${pid}-${randomHex} to avoid collisions between containers with the same PID
FlushConsumer isolates partial failures by kind: the three ingest calls (ingest, ingestSpans, ingestMetrics) now run via Promise.allSettled; the breaker records success only when all three succeed; metrics reflect per-kind reality; DLQ only counts the failed kinds on retry exhaustion. An optional FlushLogger can be injected; default falls back to console
FlushConsumerPool relaxes drain check to pendingRecords == 0: previously also required inflightRecords == 0, which never converged when another backend instance held claims on the same Redis consumer group. Consumer-task crashes are now surfaced via logger.error instead of being swallowed by Promise.allSettled. Drain poll bumped from 50 ms to 200 ms to reduce chatter
InMemoryTransport uses signal-based wakeup: dequeue no longer polls every 50 ms; enqueues wake waiters directly via a per-shard resolver list, and stop() unblocks waiters promptly. enqueueMany now batches records into a single Array.push(...entries) per shard plus one wake, eliminating the N-await bottleneck that made the in-memory buffer slower than sync on fast TimescaleDB workloads (p95 dropped from 4224 ms to 24 ms in the 3-min 100 req/s test)
Reservoir exposes getEngine() on the concrete Reservoir class (not on IReservoir): replaces the (inner as unknown as { engine }).engine cast inside ReservoirBuffered’s constructor. Kept out of the public interface so backend consumers typed against IReservoir cannot reach the engine and bypass the decorator
ReservoirBuffered.ingest/ingestSpans/ingestMetrics return the correct IngestResult shape: { ingested, failed, durationMs } instead of the previous { inserted } masked by as unknown as casts. durationMs measures the enqueue time, so the value reflects the real cost imposed on the caller
v0.9.2
Released
Feature
Metadata Filters, Admin User Creation & Org-Scoped Status URLs
Generic metadata filters across log search and alert rules, admin-driven user provisioning, editable org/project slugs, org-scoped public status page URLs (BREAKING), plus SIEM heatmap crash fix and three fastify/SvelteKit security bumps.
Generic metadata filter builder with 7 operators, backed by GIN-indexed JSONB queries and shared with alert rules
Configurable metadata.* columns in the log search table, persisted per project
Admins can create users from the dashboard (#198) without enabling public signup
BREAKING: public status page URLs moved to /status/:orgSlug/:projectSlug
Security: fastify 5.8.5 and SvelteKit 2.57.1 bumps (3 CVEs, 2 HIGH / 1 MEDIUM)
Added
Generic metadata filters in log search and alert rules: the search page and alert rule dialogs now expose a “Metadata filters” section backed by a new MetadataFilterBuilder component. Supported operators: equals, not_equals, in, not_in, exists, not_exists, contains. Filters are applied server-side via a GIN-indexed JSONB query builder in reservoir; alert evaluation also runs the same matcher in-process so rules can fire only when a specific metadata field matches
Configurable metadata columns in log table view: users can add arbitrary metadata.* keys as extra columns in the search results table via a “Columns” picker. The selected column set is persisted per project in localStorage so it survives page reloads
Create user from admin panel (#198): admins can now provision new accounts directly from Admin → User Management via a “Create User” button, without having to temporarily re-enable public signup. Opens a dialog to set email, name, password and optional admin role. Backed by a new POST /api/v1/admin/users endpoint that bypasses the auth.signup_enabled gate and logs a create_user entry to the audit log
Set a custom dashboard as default from the UI: the dashboard switcher now shows a clickable star next to each org-wide, non-personal dashboard; clicking it promotes that dashboard to be the org’s default. Backed by a new POST /api/v1/custom-dashboards/:id/set-default endpoint that atomically unsets the previous default and sets the new one in a single transaction, respecting the existing partial unique index. Personal and project-scoped dashboards are rejected with a 400
Editable project slug from monitoring page: the status-page settings card in /dashboard/monitoring now exposes a “Public URL slug” input that lets the user rename a project’s slug, with inline validation against a shared format check (lowercase alphanumeric + hyphens, 2-50 chars), reserved-word list (api, admin, dashboard, status, auth, login, signup, logout, _app, health), and per-org uniqueness. Conflicts surface as 409 with a friendly inline error; race conditions are caught at the DB layer via the new composite unique index
Editable organization slug from settings: /dashboard/settings/general slug field is no longer read-only; owners can rename the org slug with the same validation rules and global uniqueness, with a warning that any existing status-page links and embed badges will break
Changed
Public status page URL is now scoped under the organization (BREAKING): page and badge URLs changed from /status/:projectSlug to /status/:orgSlug/:projectSlug. Affects the public web page, /api/v1/status/:orgSlug/:projectSlug/badge.svg, and /api/v1/status/:orgSlug/:projectSlug/badge.json. No redirect from the old URLs. Anyone embedding the badge SVG/JSON or sharing a status-page link must update the URL to include the org slug. Migration 040 simultaneously moves project-slug uniqueness from global back to per-org, so two different organizations can now both have a project named frontend without auto-suffixing
Fixed
Security dashboard crashed with “Cannot read properties of null (reading ‘toLowerCase’)” (#200): root cause was two parallel unnest(mitre_techniques) / unnest(mitre_tactics) calls in the same SELECT list of SiemDashboardService.getMitreHeatmap. PostgreSQL evaluates sibling set-returning functions in lockstep and NULL-pads the shorter array, so any detection event whose tactic and technique arrays had different lengths produced heatmap rows with a null tactic, which then crashed MitreHeatmap.abbreviateTactic on the frontend. The heatmap query now unnests techniques only and resolves each one to its canonical tactic via the shared MITRE_TECHNIQUES map (with sub-technique to parent fallback), eliminating the malformed pairs at the source. Frontend MitreHeatmap also filters cells with null tactic/technique and DetectionEventsList.getLogLevelClass defensively handles a null level as belt-and-suspenders
Security
Bump fastify to 5.8.5 (GHSA-247c-9743-5963, CVE-2026-33806, HIGH): body schema validation could be bypassed by prepending a single space to the Content-Type header. Parser and validator disagreed on how to trim the header, so the body was still parsed but the schema lookup returned no validator and validation was skipped entirely. Upgraded from ^5.8.3 to ^5.8.5
Bump @sveltejs/kit to 2.57.1 (GHSA-2crg-3p73-43xp, CVE-2026-40073, HIGH): BODY_SIZE_LIMIT could be bypassed under certain conditions in adapter-node. Tightened the pnpm override from >=2.53.3 to >=2.57.1
Bump @sveltejs/kit to 2.57.1 (GHSA-3f6h-2hrp-w5wx, CVE-2026-40074, MEDIUM): calling redirect inside the handle hook with a location containing characters invalid for an HTTP header threw an unhandled TypeError, enabling DoS if the location included unsanitized user input
v0.9.1
Released
Fix
Security Hardening, Input Validation & Bug Fixes
SQL injection fix in querySpans, ReDoS protection for monitor assertions, 25+ bug fixes across auth, pipelines, SIEM, monitoring, and SSR safety, plus comprehensive input validation on all route params.
SQL injection fix in querySpans sortBy/sortOrder for both TimescaleDB and ClickHouse
ReDoS protection on HTTP monitor body assertion via safe-regex2
UUID validation, positive-int clamping, and max-length guards across all route params
PII salt race condition, SSE duplicate sends, and Sigma sync FK corruption fixed
localStorage SSR crash guards and URL-encoding fixes in frontend navigation
Fixed
Identifier pattern update/delete failed with “Organization ID is required” (#193): PUT and DELETE routes required organizationId as an explicit query param or API key context, but session-based auth never sets that field. Now falls back to the user’s first organization, consistent with GET and POST
Project rename failed with “Expected string, received null” (#195): updateProjectSchema used z.string().optional() which rejects null, but projects without a description send null from the DB. Changed to z.string().nullable().optional() and updated UpdateProjectInput accordingly
Pipeline create/preview/import returned generic “Validation error” (#193, #194): POST routes expected organizationId in the request body but the frontend sends it as a query param. Routes now merge the query param into the body before Zod validation. Frontend error messages now surface the first validation detail instead of the generic label
Invitation accept race condition: wrapping the membership check + insert in a transaction caused the accepted_at update to roll back when throwing “already a member”. Split the early-exit path out of the transaction and added 23505 unique-constraint handling for true concurrent accepts
SSE live tail duplicate sends: latestLog picked the oldest entry from a DESC-sorted array instead of the newest, causing every poll to re-fetch and re-send all logs since the oldest timestamp. Replaced with a defensive max-time computation
Sigma sync corrupted alert_rule_id FK: fallback alertRuleId || existing.id wrote the sigma rule’s own PK into the alert_rules FK column when no alert was auto-created. Now omits the column entirely unless a new alert rule was just created
Exception log viewer returned empty results for org-wide error groups: getLogsForErrorGroup passed an empty string as projectId to reservoir when no project filter was set. Now groups log IDs by their exception’s project_id and issues one getByIds call per project
ReDoS in HTTP monitor body assertion: user-supplied regex pattern was compiled with only a 256-char length limit, no catastrophic-backtracking check. Added safe-regex2 validation and a compile-error catch
OTLP int64 precision loss: parseInt() silently truncated int64 attribute values exceeding Number.MAX_SAFE_INTEGER. Unsafe values are now kept as strings in metadata
PII salt race condition fallthrough: if two workers raced on the first hash for an org, the loser could return an unpersisted local salt when the retry read failed, permanently desynchronizing PII hashes. Now scopes the catch to 23505 and throws on unexpected errors
Monitor status fallthrough on missing row: processCheckResult silently skipped the entire state machine (no status update, no notifications) when monitor_status was undefined. Now re-reads from DB or creates a default row before proceeding
Test isolation: auth mode pollution across test files: system_settings was never reset between tests, so any test setting auth.mode='none' caused 12 unrelated “401 without auth” assertions to return 503. Added cleanup + cache invalidation to global beforeEach
Sigma detection tests used sigma_id after code migrated to id: fixture data still passed rule.sigma_id as sigmaRuleId, which would not match the new .where('id', '=', ...) queries
translateDelete dropped level filter in reservoir: pushFilter return value was discarded for the level condition, corrupting $N parameter slots when both service and level filters were present
SQL injection in querySpans via sortBy/sortOrder: user-controlled strings were interpolated directly into raw SQL in both TimescaleDB and ClickHouse engines. Added explicit column/direction allowlists
ingestSpans hardcoded ::uuid[] for project_id: ignored the projectIdType engine option, breaking text-based project IDs with a Postgres cast error
localStorage SSR crash in organization store: 7 direct localStorage calls without a browser guard would throw ReferenceError during server-side rendering. Added browser check on all accesses
Invite token not URL-encoded in redirect: goto(/login?redirect=/invite/${token}) corrupted the redirect path for tokens containing +, =, or /. Now wraps in encodeURIComponent
ruleId not URL-encoded in security dashboard navigation: inconsistent with serviceName and technique which already used encodeURIComponent
Missing UUID validation on monitoring route params: all /:id handlers passed request.params.id directly to DB queries without format validation. Added Zod .uuid() parsing on every route
Negative limit/days in monitoring routes: Number("-1") || 50 evaluates to -1 (truthy), passing a negative value to LIMIT. Replaced with a parsePositiveInt guard that clamps to [1, max]
Missing UUID validation on status-incident route params: same issue as monitoring - :id params were used unvalidated in DB queries
SIEM comment body has no max length: z.string().min(1) with no upper bound allowed arbitrarily large comment payloads. Added .max(10000)
Notifications and alerts limit/offset NaN passthrough: parseInt("abc") returned NaN, which was passed to Kysely .limit(NaN). Added safe integer parsing with fallback and max cap
Correlation referenceTime accepted invalid date strings: schema validated only {type: 'string'}, so new Date("garbage") flowed into Kysely WHERE clauses as Invalid Date. Added format: 'date-time' and a defensive 400 response
Log pipeline comment contradicted jsonb merge direction: code comment said “do NOT overwrite existing” but the in-progress fix had flipped the jsonb || operand order so pipeline fields now win. Updated comment to match actual behavior
v0.9.0
Released
Feature
Service Monitoring, Log Pipelines & Custom Dashboards
Three major features: uptime monitoring with public status pages (#152), log parsing and enrichment pipelines with grok and GeoIP, and fully customizable dashboards with 9 panel types (#151).
Service health monitoring with HTTP/TCP/heartbeat checks and public Uptime Kuma-style status pages
Log parsing pipelines with 5 built-in parsers, custom grok patterns, and GeoIP enrichment
Custom dashboards with 9 panel types, drag-and-drop reorder, resize, and YAML import/export
Scheduled maintenances and manual status incidents on public status pages
Versioned dashboard schema with migration framework and cross-org isolation guards
Added
Service health monitoring and status pages (#152)
Proactive uptime monitoring with auto-generated public status pages.
3 monitor types: HTTP/HTTPS (configurable method, expected status, headers, body assertion), TCP ping, and heartbeat (alert when no ping received within grace window)
HTTP config: per-monitor httpConfig with method, expectedStatus, custom headers, and body assertion (contains or regex) - stored as JSONB, validated via Zod
Per-monitor severity: incident severity is configurable per monitor (critical, high, medium, low, informational) instead of hardcoded high
BullMQ-style polling: worker checks all due monitors every 30s, batched in groups of 20 concurrent checks via Promise.allSettled
TimescaleDB storage: monitor_results hypertable with 7-day compression, 30-day retention, and monitor_uptime_daily continuous aggregate refreshed hourly
State machine: consecutive failure tracking with configurable threshold, atomic incident dedup guard (WHERE incident_id IS NULL), auto-resolve on recovery
Auto-incident creation: when failure threshold is crossed, a SIEM incident is created with source: 'monitor' and linked via monitor_id; notifications sent via existing email/webhook channels
Public status page (/status/:projectSlug): Uptime Kuma-inspired design with 45-day heartbeat bar grid, per-monitor uptime badge, overall status banner, custom CSS tooltips, light/dark mode toggle
Status page access control: configurable visibility per project - disabled (default), public, password-protected, or org-members-only
Scheduled maintenances: create maintenance windows with start/end times; active maintenances suppress monitor incident creation and display a banner on the status page
Manual status incidents: create public incident communications (investigating → identified → monitoring → resolved) with update timeline, independent from SIEM incidents
Heartbeat endpoint: POST /api/v1/monitors/:id/heartbeat accepts both API key and session auth, rate-limited to 600/min
Project slugs: auto-generated from project name on creation, unique per org, backfilled for existing projects via migration
Dashboard UI (/dashboard/monitoring): monitor list with project selector, create/edit/delete forms with client-side validation, detail page with refresh button, uptime chart, recent checks, copy heartbeat URL
Monitoring navigation: added to sidebar under “Detect” section alongside Alerts and Security
Log parsing and enrichment pipelines
Define multi-step processing rules that automatically parse and enrich incoming log messages before they are stored.
5 built-in parsers: nginx (combined log format), apache (identical to nginx), syslog (RFC 3164 and RFC 5424), logfmt, and JSON message body
Custom grok patterns: %{PATTERN:field} and %{PATTERN:field:type} syntax with 22 built-in patterns (IPV4, WORD, NOTSPACE, NUMBER, POSINT, DATA, GREEDYDATA, QUOTEDSTRING, METHOD, URIPATH, HTTPDATE, etc.) and optional type coercion (:int, :float)
GeoIP enrichment: extract country, city, coordinates, timezone, and ISP data from any IP field using the embedded MaxMind GeoLite2 database
Async processing via BullMQ: pipelines run as background jobs after ingestion - zero impact on ingestion latency
Project-scoped vs org-wide: pipelines can target a specific project or apply to all projects in the organization; project-specific pipelines take priority over org-wide ones
Pipeline preview: test any combination of steps against a sample log message and inspect per-step extracted fields and the final merged result before saving
YAML import/export: import pipeline definitions from YAML with name, description, enabled, and steps fields; upserts (replace existing pipeline for the same scope)
In-memory cache: getForProject caches the resolved pipeline per project for 5 minutes, automatically invalidated on create/update/delete
Settings UI (/dashboard/settings/pipelines): list, enable/disable toggle, create, edit, and delete pipelines with live org-switch reactivity ($effect instead of onMount)
Step builder: interactive UI for adding, reordering, and configuring parser, grok, and geoip steps with per-type configuration forms
Pipeline edit page redirects to the list when the active organization is switched, preventing stale-ID errors
Custom dashboards with configurable panels (#151)
User-built dashboards replace the previous fixed /dashboard page, with team-specific views across all observability domains.
9 panel types covering every data source: time series, single stat, top-N table, live log stream, alert status (logs/alerts), metric chart and metric stat (OTLP metrics with avg/sum/min/max/count/last/p50/p95/p99 aggregations), trace latency (p50/p95/p99 from spans), detection events (SIEM by severity), monitor status (uptime + response time)
Panel registry architecture: adding a new panel type touches only 6 files (shared types, backend Zod schema, backend fetcher, frontend panel component, frontend config form, frontend registry entry); the renderer, container, store, and routes never need to change
Drag-and-drop reorder via svelte-dnd-action with optimistic local state and a single PUT save
Drag-to-resize with bottom-right pointer-event handle, snapping to grid units; constrained by per-type min width/height from the registry
Responsive 12/6/1 column grid that collapses panels to 6 columns on tablet (640-1024px) and 1 column on mobile (<640px); stored widths are always in the canonical 12-col reference and scale proportionally
Auto-created Default dashboard per organization, idempotent via Postgres unique-violation guard, replicating the previous fixed layout (4 stat cards + log volume + top services + top error messages) so existing users see no visual change
Inline edit mode with toggle, no separate edit page; pending changes are kept in a snapshot and discarded on Cancel
Per-panel configuration dialogs with type-specific forms (level toggles, intervals, aggregation pickers, percentile selectors)
Dashboard switcher dropdown in the page header with personal/shared distinction, create, delete (default protected), import, export
YAML import/export: dashboards round-trip through YAML for version-controlling alongside infrastructure code; import regenerates panel IDs and uses JSON_SCHEMA to block JS-tag prototype pollution
Versioned JSON schema (schema_version: 1) with a migration framework in @logtide/shared: each version writes a MigrationFn indexed by target version, migrateDashboard walks the chain on every read; clamps out-of-range versions defensively
Cross-org isolation guard: every panel data fetch verifies that config.projectId belongs to the requesting org, preventing data leaks via crafted YAML imports or stale references
Batch panel data endpoint (POST /:id/panels/data): single round-trip fetches all panel data via Promise.allSettled, individual panel errors do not fail the dashboard
Organization scoping: dashboards are org-scoped with optional is_personal flag (only visible to creator) and created_by tracking; partial unique indexes prevent multiple defaults per (org, project) scope
Migration 039_custom_dashboards.sql: JSONB panels column with GIN index for future panel-type filtering, partial unique indexes for default scope guarantees
Fixed
Status page slug collision: getPublicStatus now filters by status_page_public flag instead of returning the first project matching the slug, preventing cross-org data leaks
createMonitor not transactional: monitor and monitor_status inserts are now wrapped in db.transaction() to prevent orphaned monitors
mapMonitor typed: replaced any parameter with proper MonitorWithStatusRow interface for compile-time safety
Org membership check optimized: monitoring routes now use a single SELECT WHERE user_id AND organization_id query instead of fetching all user orgs and scanning in JS
Redundant DB read eliminated: processCheckResult now receives status data from the already-fetched monitor object instead of issuing a second SELECT
Target validation on update: PUT endpoint now validates target format against monitor type (HTTP must start with http:///https://, TCP must contain :)
$derived.by fix: monitor detail page uptime calculation now uses $derived.by() instead of $derived(() => ...) for correct Svelte 5 reactivity
@const placement: replaced invalid {@const} inside <div> elements with {#if}/{:else} blocks for Svelte 5 compatibility
uptimePct type coercion: Postgres ROUND() returns numeric as string - status page now coerces to number before calling .toFixed()
Default failureThreshold aligned: frontend form default changed from 3 to 2 to match backend default
Test setup cleanup: added monitor_results, monitor_status, monitors, incident_alerts to global beforeEach cleanup
Quick Start cURL example now validates without a manual timestamp, Sigma worker no longer floods logs on empty detection batches, and fresh instances without INITIAL_ADMIN_* env vars auto-promote the first registered user to admin.
logSchema now defaults time to current ISO string so minimal payloads and copy-paste examples validate
Sigma worker [SigmaDetection] No matches found log gated behind DEBUG_SIGMA=true
First registered user is auto-promoted to admin when no INITIAL_ADMIN_* is set and no admin exists
Fixed
Quick Start cURL example failed validation: the empty-state code snippet sent {logs: [{level, service, message}]} without a time field, but logSchema required it for the standard ingestion path (only the array-format path ran normalizeLogData). The schema now defaults time to the current ISO string when missing, so copy-paste examples and minimal payloads validate without requiring users to inject a timestamp.
Noisy Sigma worker logs: [SigmaDetection] No matches found was emitted at info level on every batch with no detections, flooding worker output in normal operation. The line is now gated behind DEBUG_SIGMA=true so it only appears when explicitly opted in.
*No admin user when INITIAL_ADMIN_ not set (#188)**: on a fresh instance without INITIAL_ADMIN_EMAIL/INITIAL_ADMIN_PASSWORD, no usable admin existed and admin settings were unreachable. The bootstrap no longer creates a system fallback user; instead, the first user to register (via /register or external auth provider) is automatically promoted to admin if no admin exists yet.
Fixed ClickHouse traces/metrics data-availability queries failing due to raw epoch date parameters, and resolved stale dashboard sessions after volume resets.
ClickHouse traces/metrics data-availability queries now use toDateTime64() clamp instead of raw epoch dates
Dashboard validates session token against backend on load and auto-logs out on invalid session
Fixed
ClickHouse traces/metrics data-availability always empty: queryTraces and queryMetrics passed raw 0 for epoch dates as DateTime64(3) parameter, which ClickHouse can’t parse; now uses the same toDateTime64() clamp used by log queries
Stale session after volume reset: dashboard only checked localStorage for a token without validating it against the backend; now calls /auth/me on load and auto-logs out if the session is invalid
Five cross-org and auth security fixes, corrected SSE real-time streams, resolved 20+ Svelte memory leaks on navigation, and multiple bug fixes across SIEM, incidents, webhooks, and retention jobs.
Cross-org isolation and auth bypass fixes in SIEM and pattern routes
SSRF protection for legacy webhook path and disabled-user login blocked
SSE real-time events and log stream duplicate emission fixed
20+ Svelte memory leaks from unsubscribed auth stores resolved
Docker config sync and dependency security bumps (picomatch, brace-expansion, fast-xml-parser)
Security
Cross-org isolation fix in SIEM: linkDetectionEventsToIncident now scopes detection events to the requesting organization, preventing cross-tenant data corruption via crafted API calls
Cross-org auth bypass in pattern routes: PUT and DELETE handlers for correlation patterns now verify organization membership before mutating data (same check GET/POST already had)
SSRF protection for legacy webhook path: the alert-notification job’s direct fetch() call now validates URLs against private/internal IP ranges, matching the WebhookProvider safeguard
Disabled user login blocked: POST /login now checks the disabled flag before creating a session, preventing disabled accounts from obtaining tokens
Expired invitation info leak: getInvitationByToken now filters on expires_at > NOW(), preventing enumeration of expired invitation details
Fixed
SIEM dashboard timeline crash: time_bucket() call was missing ::interval cast on the parameterized bucket width, causing a PostgreSQL type error that broke the timeline widget for all users
SSE real-time events broken: SIEM store and incident detail page read auth token from localStorage('session_token') (wrong key), so the SSE connection never authenticated; now uses getAuthToken() from the shared auth utility
SSE log stream duplicate emission: when multiple logs shared the same timestamp, the inclusive from bound caused them to be re-sent on every poll tick; stream now tracks sent log IDs to deduplicate
Incident severity auto-grouping wrong: MAX(severity) used PostgreSQL alphabetical ordering (medium > critical), producing incorrect severity on auto-grouped incidents; now uses ordinal ranking
Sigma notification failures silent: notification job payload was missing organization_id and project_id, and markAsNotified was called with null historyId; both now handled correctly
Incidents pagination total always zero: loadIncidents in the SIEM store never wrote response.total to incidentsTotal
Memory leaks on navigation: 20+ Svelte components called authStore.subscribe() without cleanup; all now store the unsubscribe function and call it in onDestroy
offset=0 silently dropped: API client functions used if (filters.offset) which is falsy for zero, so page-1 requests never sent the offset parameter; changed to if (filters.offset != null)
Search debounce timer leak: searchDebounceTimer was not cleared in onDestroy, causing post-unmount API calls when navigating away mid-search
verifyProjectAccess double call: when projectId is an array, the first element was verified twice (once before the loop, once inside it); consolidated into a single loop
updateIncident silent field skip: title, severity, and status used truthy checks (&&) instead of !== undefined, inconsistent with description and assigneeId
Webhook error messages empty: response.statusText is empty for HTTP/2; error now reads the response body for useful detail
Retention job crash on empty orgs: Math.max(...[]) returns -Infinity, cascading to an Invalid Date in the drop_chunks call; early return added when no organizations exist
escapeHtml DOM leak: PDF export’s escapeHtml created orphaned DOM nodes in the parent document; replaced with pure string replacement
Webhook headers validation missing: CreateChannelDialog silently swallowed invalid JSON in the custom headers field; now validates on submit
getIncidentDetections no org scope: query now accepts optional organizationId for defense-in-depth filtering
Stale shared package types: dist contained outdated Project and Incident interfaces with phantom fields (slug, statusPageVisibility, source, monitorId); rebuilt from source
Changed
Docker config sync: docker-compose.build.yml now matches docker-compose.yml with all environment variables (MongoDB, TRUST_PROXY, FRONTEND_URL, INTERNAL_DSN, DOCKER_CONTAINER), MongoDB service, and fluent-bit-metrics service
NODE_ENV for backend: production docker-compose.yml now sets NODE_ENV: production on the backend service (worker and frontend already had it)
docker/.env.example: added STORAGE_ENGINE, ClickHouse, and MongoDB configuration sections
Dependencies
picomatch 4.0.3 → 4.0.4 (fix ReDoS via extglob quantifiers + POSIX character class method injection)
All dashboard pages now show content-shaped skeleton loaders, automated Helm chart releases on every stable Docker image, and multiple performance and correctness fixes for API endpoints and admin pages.
Skeleton loaders and loading overlays on all dashboard pages
Automated Helm chart releases triggered on every stable Docker image release
API 400 responses now include field-level validation errors
Fixed admin pages returning 502 on direct load/reload
POST /api/v1/logs/identifiers/batch performance: bypasses storage engine, one PostgreSQL query
GET /api/v1/logs/hostnames 8+ second queries fixed with engine-specific indexes and a 6h window cap
Added
Skeleton loaders and loading overlays: all dashboard pages now show content-shaped loading states instead of blank spinners
New Skeleton, SkeletonTable, and TableLoadingOverlay components (src/lib/components/ui/skeleton/)
Directional shimmer animation via @keyframes shimmer using design tokens — works in light and dark mode, disabled for prefers-reduced-motion
Initial load (no data yet): animated skeleton rows mirroring the page layout — stat cards on /dashboard, project cards on /dashboard/projects, table rows on search, traces, errors, admin tables, incidents, alerts history, and members
Re-fetch (filter change, pagination): existing content dims with a translucent overlay and centered spinner, preventing layout shift and context loss
Automated Helm chart releases: every stable Docker image release now triggers a repository_dispatch to logtide-dev/logtide-helm-chart, which auto-bumps appVersion and chart version (patch), commits, and publishes a new chart release to the Helm repo on GitHub Pages
Fixed
API 400 responses now include a details array with field-level validation errors instead of just a generic message. Covers both Fastify/AJV schema validation and Zod validation errors (including uncaught ZodError that previously returned 500)
Admin pages returned 502 Bad Gateway on direct load/reload: the admin layout ([email protected]) breaks out of the dashboard layout chain, so ssr = false was not inherited; added a dedicated +layout.ts to the admin section
/dashboard/admin/projects/[id] crashed with “Something went wrong” due to formatDate being called but not defined (function was named formatTimestamp)
POST /api/v1/logs/identifiers/batch slow: the route was calling reservoir.getByIds (hitting ClickHouse/TimescaleDB/MongoDB) only to verify project access, then querying log_identifiers (PostgreSQL) separately. Since log_identifiers already stores log_id → project_id + identifier data, the storage engine call is now bypassed entirely — one PostgreSQL query replaces the N×storage-engine-roundtrips loop. Added bloom filter skip index on id in ClickHouse and a standalone id index in TimescaleDB (migration 032) for getByIds used by findCorrelatedLogs
GET /api/v1/logs/hostnames taking 8+ seconds: the 6h window cap was only applied when from was absent — explicit from params (e.g. 24h range from the search page) bypassed it and triggered a full-range metadata scan; cap now clamps any window to 6h max. Added limit: 500 to the distinct call. Per-engine optimizations: ClickHouse adds a hostname materialized column (computed at ingest, eliminates JSONExtractString at query time) and uses it directly in distinct queries; TimescaleDB adds a composite expression index (project_id, (metadata->>'hostname'), time) (migration 032); MongoDB adds a sparse compound index on metadata.hostname. All three engines also now extract the metadata field in a subquery (once per row vs 3×)
v0.8.3
Released
Feature
Comprehensive Audit Logging & OIDC Brand Icons
Major expansion of the audit trail system covering all critical platform actions for GDPR/SOC2 compliance, OIDC provider brand icons, and fixes for date formatting localization and data availability routing.
Audit logging for log access, auth events, identity management, provider config, settings, and sessions
Backend auto-detects OIDC provider icon from issuer URL
Date/number formatting now respects user's browser/system locale
Fixed data-availability endpoint for ClickHouse and MongoDB storage engines
Added
Comprehensive Audit Logging: major expansion of the audit trail system to cover all critical platform actions for improved compliance (GDPR/SOC2) and security monitoring.
Log Access Auditing: every log search, trace view, context lookup, single log detail view, and live stream connection is now recorded with user identity, IP address, and query parameters.
External Authentication Auditing: successful logins via OIDC and LDAP providers are now tracked, including new user registration events.
Identity Management Auditing: linking and unlinking of external identities (Google, GitHub, LDAP, etc.) to user accounts is now recorded.
Authentication Provider Auditing: all administrative actions on auth providers (create, update, delete, reorder) are now fully audited with configuration change summaries.
System Settings Auditing: any changes to global platform settings (auth mode, signup status, default users) are now tracked with before/after metadata.
Session Auditing: viewing of active session lists and individual session event timelines is now recorded.
Audit metadata now includes detailed context like search queries (q), filter parameters, and updated keys for configuration changes.
OIDC login page now shows brand icons for well-known providers (Google, Microsoft/Azure, GitHub, GitLab, Okta, Auth0, Keycloak, Authentik); unknown providers fall back to the generic icon
Backend auto-detects the provider icon from the issuer URL when creating or updating an OIDC provider, with name/slug matching as fallback for self-hosted setups
Fixed
Date and number formatting localization: removed hardcoded locales (it-IT, en-US) from the frontend (SIEM, Search, Admin, etc.) to ensure the application automatically respects the user’s browser/system language settings.
GET /api/v1/projects/data-availability returned logs: [] (and incorrect traces/metrics) when STORAGE_ENGINE=clickhouse or mongodb; the endpoint now routes all three checks through the reservoir so they hit the correct backend
v0.8.2
Released
Fix
SigmaHQ Auto-Sync, Trace Links & Security Fixes
Daily auto-sync for SigmaHQ community rules, trace navigation from log detail panel, audit logs for alert rules, and security/robustness fixes for ingestion and admin endpoints.
SigmaHQ rules auto-sync daily at 2:30 AM
View Trace link in log detail panel when trace_id is present
Audit log entries for alert rule create/update/delete
Admin pagination capped at 200 to prevent oversized allocations
NDJSON lines exceeding 1MB rejected with HTTP 400
api_key_id removed from log metadata (information disclosure fix)
Added
SigmaHQ auto-sync: SigmaHQ community rules now auto-sync daily at 2:30 AM for organizations that have existing community rules enabled.
Trace link in log detail panel: The log detail panel now shows a “View Trace →” link that navigates directly to the trace timeline when a trace_id is present in the log.
Audit logs for alert rules: Create, update, and delete operations on alert rules now generate audit log entries.
Fixed
Admin pagination cap: The limit parameter on admin list endpoints is now capped at 200, preventing oversized result set allocation.
NDJSON ingestion size limit: Individual lines exceeding 1 MB in NDJSON payloads are now rejected with HTTP 400 instead of being silently processed.
Log metadata information disclosure: api_key_id is no longer stored in log metadata. It was previously injected in v0.8.1 for exception log display but exposed sensitive data; the display now resolves the key name at read time instead.
v0.8.1
Released
Breaking
Project Visibility in Errors & Storage Engine Fix
Project name display in error pages, API Key visibility in exception logs, and a fix for data-availability routing when using ClickHouse or MongoDB.
All existing API Keys have been invalidated — regenerate your keys
Project name shown in Exceptions list and error group detail pages
API Key name visible in recent exception logs
Data-availability endpoint now respects storage engine selection
Search page project filter fixed for empty logs array
Breaking Changes
All API Keys have been invalidated: All existing API Keys were deleted as part of this release. You must regenerate your API Keys from the dashboard and update your SDKs and ingestion clients accordingly.
Added
Project visibility in Exceptions: The /dashboard/errors list and the individual error group detail pages now explicitly display the name of the project that generated the error.
API Key visibility in Exception logs: The recent logs tab within an error group detail page now displays the specific API Key name used to ingest the log. Ingestion now injects the api_key_id into log metadata.
Fixed
Project data-availability ignoring storage engine: GET /api/v1/projects/data-availability was always querying the PostgreSQL logs table via Kysely, returning logs: [] when STORAGE_ENGINE was set to clickhouse or mongodb. The logs check now uses reservoir.distinct() which routes to the correct storage backend.
Search page showing no projects when logs is empty array: the project filter guard logsProjectIds ? was truthy for [], filtering out all projects. Changed to logsProjectIds?.length so an empty array correctly falls back to showing all projects.
MongoDB storage adapter for @logtide/reservoir, browser and frontend SDKs with source maps and Core Web Vitals, and OTLP metrics dashboards with cross-signal correlation.
MongoDB storage adapter for @logtide/reservoir
Browser/Frontend SDKs with session tracking & source maps
Core Web Vitals and network breadcrumbs
OTLP Metrics rollups & Golden Signals (P50/P95/P99)
Correlation across logs, traces, and metrics
Added
Browser & Frontend SDK Enhancements — Sentry-level browser observability across all frontend framework SDKs.
@logtide/browser: Dedicated browser SDK with session tracking, Web Vitals, and offline resilience.
Source Maps: @logtide/cli for source map uploads and backend un-minification service.
Frameworks: Deep integrations for Next.js, Nuxt, SvelteKit, and Angular.
Metrics Dashboard & Rollups — Redesigned metrics page with pre-aggregated rollups for TimescaleDB and ClickHouse.
MongoDB Storage Adapter — Full MongoDB backend for the @logtide/reservoir storage layer.
Golden Signals with Percentiles — P50/P95/P99 percentile aggregation across all storage engines.
Smart Project Selectors — Dropdowns now only show projects that have data in the relevant category.
Optimized
TimescaleDB Skip-Scan — Recursive CTEs for instant distinct queries on high-cardinality fields.
Intelligent Volume Estimation — countEstimate support for instant dashboard loads on high-volume projects.
Batch Ingestion — Ordered-false inserts and tuned connection pools for maximum throughput.
ClickHouse Projections — Faster query execution for common search patterns.
Fixed
Internal Logging Plugin — Fixed DSN passing to @logtide/fastify for self-monitoring.
Backend Self-Monitoring — Improved DSN construction and verbose startup logging.
Docker Compose — Fixed missing environment variables and worker health check race conditions.
Admin Dashboard — Fixed missing metrics series in platform activity chart.
v0.7.0
Released
Feature
OTLP Metrics, Service Graph & Audit Log
OpenTelemetry metrics ingestion, service dependency graph visualization, audit logging, and major UX restructuring.
OTLP Metrics with protobuf/JSON support
Service Dependency Graph with force-directed layout
Audit Log for compliance tracking
UX sidebar restructured into Observe/Detect/Manage
46 TypeScript/Svelte warnings eliminated
Added
OTLP Metrics Ingestion — Full OpenTelemetry metrics support with protobuf/JSON, all 5 metric types, exemplar support, TimescaleDB hypertables, ClickHouse support, query API with 7 aggregation intervals and 6 functions, group-by label support, 118+ tests
Service Dependency Graph — Force-directed graph visualization with health color-coding, click-to-inspect panels, and PNG export
Audit Log — Tracks 4 event categories (login, config changes, user management, data modifications) with high-performance buffer and CSV export
Changed
Batch ingestion endpoint accepts flexible payload formats (standard, direct array, wrapped array) with auto-normalization
UX Restructuring — Sidebar grouped into Observe/Detect/Manage sections, Service Map merged into Traces, Sigma Rules moved to Security, Settings restructured, Command palette updated
Fixed
OTLP Traces typo using resource_logs instead of resource_spans
OTLP Authentication for /v1/otlp routes
JavaScript SDKs updated to v0.6.1 for OTLP compatibility
Frontend environment loading via $env/dynamic/public
SDK code examples across dashboard
Pagination total count with fast approximate estimates
SMTP no longer requires user/password credentials. Only the host is needed, and the from address uses the SMTP_FROM parameter.
SMTP works without USER/PASS credentials
From address uses SMTP_FROM parameter
Fixed SMTP configuration to support unauthenticated mail servers. Only SMTP_HOST is now required — SMTP_USER and SMTP_PASS are optional. The sender address is configured via SMTP_FROM.
v0.6.2
Released
Breaking
Write-Only API Keys & Domain Allowlists
New write-only API key type safe for browsers and mobile, plus domain/IP allowlists with wildcard subdomain support for origin validation.
Write-only API keys safe for client-side use
Domain/IP allowlist with wildcard subdomains
Dogfooding SDK migration to official plugins
fast-xml-parser security fix
Added
Write-Only API Keys — New type field (write/full), safe for browsers and mobile, defaults to write
Domain/IP Allowlist — Up to 50 allowed origins per key with wildcard subdomain support and Origin header validation
Security
fast-xml-parser bumped to >=5.3.6 for entity expansion DoS fix
Read endpoints reject write-only API keys with 403
Origin allowlist validation with wildcard subdomain parsing
Breaking Changes
API key default type changed to write — existing keys migrated automatically, server-side queries need full type
ClickHouse as a full alternative to TimescaleDB via the @logtide/reservoir abstraction layer, with factory pattern engine selection and full query migration.
ClickHouse via @logtide/reservoir abstraction
Factory pattern engine selection
26 integration tests against both engines
Full log query migration to Reservoir
Added
ClickHouse Storage Engine — Full support as TimescaleDB alternative via @logtide/reservoir abstraction layer with PREWHERE, async_insert, and ngrambf_v1 indexes
Full Log Query Migration — All query operations (alerts, dashboard, admin, retention, ingestion, correlation) migrated to the Reservoir abstraction
Performance
Removed COUNT(*) full scans in admin queries, switched to continuous aggregates
ClickHouse DateTime64(3) millisecond precision with hasToken() fallback
PII Masking at Ingestion — Content patterns (email, credit card, phone, SSN, IPv4, API keys), field name masking, custom rules, three strategies (mask, redact, hash with per-org salt), Settings UI with live test panel
Terminal log view with ANSI color coding, pre-configured detection packs, event correlation by identifier, alert preview testing, and optional Redis dependency.
Terminal Log View with ANSI color coding
Pre-configured Sigma detection packs
Event correlation by request/trace/user ID
Alert preview with 'Would Have Fired' simulation
Redis dependency now optional
Added
Terminal Log View — ANSI-style color coding for a familiar terminal experience
Detection Packs — Pre-configured Sigma rule bundles for common scenarios (startup, auth, database health)
Event Correlation — Click any request ID, trace ID, or user ID to see all related logs
Alert Preview — “Would Have Fired” simulation to test rules before enabling
Optional Redis — PostgreSQL-based alternatives with adapter pattern queue system
Fixed
Log Context modal reopening after close
Exception details from metadata display
WebSocket memory leak in live tail handler
SQL injection prevention in notification publisher
v0.4.2
Released
Fix
Clipboard Utility & Config Validation
Centralized clipboard copy function, config validation test coverage, and documentation corrections.
Centralized clipboard utility
Config validation test coverage
Added
Clipboard utility with centralized copy function
Config validation test coverage
Fixed
Documentation corrections for API configuration
Docker Compose configuration information
v0.4.1
Released
Improvement
Multi-Language Exception Parsers
Stack trace parsing support for multiple programming languages and dependency updates including @sveltejs/kit.
Multi-language stack trace parsing
SvelteKit and dependency updates
Added
Exception parsers for multi-language stack trace parsing
Changed
Dependencies updated including @sveltejs/kit
Fixed
OTLP endpoint URLs in documentation
v0.4.0
Released
Breaking
LogWard to LogTide Rebrand & Search
Project rebranding from LogWard to LogTide, substring search with trigram indexes, clickable dashboard elements, exception visualization, and customizable retention.
Project rebranded from LogWard to LogTide
Substring search with PostgreSQL trigram index
Clickable dashboard elements
Enhanced exception & stack trace visualization
Customizable log retention policy
Added
Substring Search — Full-text search with PostgreSQL trigram index
Clickable Dashboard Elements — Interactive navigation from charts and widgets
Customizable Log Retention — Configurable retention policies per project
Full-Page Export — Export all matching logs across pages
Custom Time Range Picker — Improved date/time selection UI
Breaking Changes
Environment variables renamed (LOGWARD → LOGTIDE)
Fluent Bit configuration variables renamed
Database defaults changed
Docker container and service names changed
SMTP default sender changed
Fixed
Mobile navigation menu hamburger functionality
Services dropdown showing all services
Journald log format detection
Syslog level mapping improvements
OTLP protobuf parsing with proper binary support
2025
v0.3.2
Released
Fix
SvelteKit 2 Compatibility Fixes
Fixed SvelteKit 2 compatibility with new store patterns, traces page navigation 404s, and registration error handling.
SvelteKit 2 store pattern compatibility
Traces page 404 navigation fix
Fixed
SvelteKit 2 compatibility with new store patterns
Traces page navigation fixing 404 links
Registration error network handling
v0.3.3
Released
Feature
LDAP, OIDC & Auth-Free Mode
Enterprise authentication with LDAP and OpenID Connect, auth-free mode for home labs, initial admin via environment variables, and ARM64 Docker builds.
Syslog integration documentation with device-specific guides, Go SDK documentation, and documentation restructure with new Integrations section.
Syslog integration with device guides
Go SDK documentation
Documentation restructure
Runtime PUBLIC_API_URL configuration
Added
Syslog integration documentation with device-specific guides
Go SDK documentation at /docs/sdks/go
Documentation restructure with new Integrations section
Changed
Docker Compose improved container orchestration
Onboarding flow skip behavior refinement
Runtime configuration for PUBLIC_API_URL
Fixed
Sign Up Free link pointing to correct route
Skip tutorial redirect loop
API URL in code examples
v0.2.3
Released
Improvement
Docker Image Publishing & Self-Hosting Docs
Automated Docker image publishing via GitHub Actions CI/CD and comprehensive self-hosting deployment documentation.
Docker images via GitHub Actions CI/CD
Self-hosting deployment documentation
Pre-built images in docker-compose.yml
Added
Docker image publishing with GitHub Actions CI/CD pipeline
Self-hosting documentation with deployment guides
Changed
docker-compose.yml now uses pre-built images by default
v0.2.2
Released
Feature
Onboarding Tutorial & Empty States
Multi-step onboarding wizard with progress tracking, empty state components for guidance, and expanded testing infrastructure.
Multi-step onboarding wizard
User onboarding checklist with progress
Empty state guidance components
Expanded testing infrastructure
Added
Onboarding Tutorial — Multi-step wizard guiding new users through setup
Empty State Components — Helpful guidance when dashboards have no data yet
User Onboarding Checklist — Progress tracking for initial setup steps
UI enhancements with help components
Fixed
Organization context handling improvements
Error states and loading indicators
v0.2.1
Released
Improvement
Redis Caching & 50x Performance Boost
Type-safe Redis caching layer with session validation 30x faster, API key verification 20x faster, query results 10x faster, and aggregations 50x faster.
Session validation 30x faster
API key verification 20x faster
Query results 10x faster
Aggregations 50x faster
Added
Redis Caching Layer — Type-safe cache keys with automatic invalidation
Landing page public index
Changed
Database optimization with composite indexes
Performance
Session validation 30x faster with caching
API key verification 20x faster
Query results 10x faster
Aggregations 50x faster
Fixed
Admin panel double sidebar issue
Admin routes navigation path corrections
v0.2.0
Released
Feature
OpenTelemetry & Distributed Tracing
Full OpenTelemetry support with OTLP endpoints, distributed tracing with complete CRUD operations, and 563+ tests.
OpenTelemetry OTLP endpoints
Distributed tracing with full CRUD
563+ tests
Keyboard navigation for span selection
Added
OpenTelemetry Support — OTLP ingestion endpoints for traces and logs
Distributed Tracing — Full CRUD operations with waterfall visualization
Testing Infrastructure — 563+ tests covering all major features
Changed
OTLP ingestion performance optimization
Span selection UX with keyboard navigation
Fixed
Frontend UX issues in OTLP data display
trace_id handling flexibility for various formats
v0.1.0
Released
Feature
Initial Release
First public release of LogTide with multi-organization architecture, batch log ingestion, real-time streaming, TimescaleDB storage, Sigma detection engine, and official SDKs.
Multi-organization architecture
High-performance batch log ingestion
Real-time log streaming via SSE
Sigma detection engine
Node.js, Python, PHP & Kotlin SDKs
Docker Compose deployment
Added
Multi-organization architecture — Isolated workspaces for teams and projects
High-performance batch log ingestion — Optimized for high throughput
Real-time log streaming — Server-Sent Events for live tail
Advanced search and filtering — Query logs with complex filters
TimescaleDB storage — Automatic compression and retention policies
Dashboard with statistics — Overview of log volume, errors, and trends
Alert system — Rules with email and webhook notifications