Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ NODE_ENV=development
CLICKHOUSE_URL=http://default:password@localhost:8123
RUN_REPLICATION_CLICKHOUSE_URL=http://default:password@localhost:8123
RUN_REPLICATION_ENABLED=1
# LOGS_SEARCH_PROJECTOR_ENABLED=1
# LOGS_SEARCH_PROJECTOR_PREVIEW_ENABLED=1
# Store task run spans/traces in ClickHouse so the dashboard trace view is
# populated in local dev. The local stack is ClickHouse-backed (see above), so
# leaving this unset falls back to the "postgres" store and dev run traces show
Expand Down
6 changes: 6 additions & 0 deletions .server-changes/improve-global-log-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Global log search now supports faster bounded substring matching and clearer time-range expansion.
104 changes: 54 additions & 50 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ export function SideMenu({
});
}

if (isAdmin || featureFlags.hasQueryAccess) {
if (isAdmin || featureFlags.hasQueryAccess || featureFlags.hasLogsPageAccess) {
staticSections.push({
id: "metrics",
title: "Observability",
Expand All @@ -843,55 +843,59 @@ export function SideMenu({
} satisfies SideMenuItemConfig,
]
: []),
{
id: "errors",
name: "Errors",
icon: BugIcon,
activeIconColor: "text-errors",
to: v3ErrorsPath(organization, project, environment),
dataAction: "errors",
},
{
id: "query",
name: "Query",
icon: CodeSquareIcon,
activeIconColor: "text-query",
to: queryPath(organization, project, environment),
dataAction: "query",
},
{
id: "queues",
name: "Queues",
icon: QueuesIcon,
activeIconColor: "text-queues",
to: v3QueuesPath(organization, project, environment),
dataAction: "queues",
},
{
id: "dashboards",
name: "Dashboards",
icon: ChartBarIcon,
activeIconColor: "text-metrics",
to: v3DashboardsLandingPath(organization, project, environment),
dataAction: "dashboards-landing",
action: (
<CreateDashboardButton
organization={organization}
project={project}
environment={environment}
isCollapsed={isCollapsed}
/>
),
after: (
<DashboardList
organization={organization}
project={project}
environment={environment}
isCollapsed={isCollapsed}
user={user}
/>
),
},
...(isAdmin || featureFlags.hasQueryAccess
? [
{
id: "errors",
name: "Errors",
icon: BugIcon,
activeIconColor: "text-errors",
to: v3ErrorsPath(organization, project, environment),
dataAction: "errors",
},
{
id: "query",
name: "Query",
icon: CodeSquareIcon,
activeIconColor: "text-query",
to: queryPath(organization, project, environment),
dataAction: "query",
},
{
id: "queues",
name: "Queues",
icon: QueuesIcon,
activeIconColor: "text-queues",
to: v3QueuesPath(organization, project, environment),
dataAction: "queues",
},
{
id: "dashboards",
name: "Dashboards",
icon: ChartBarIcon,
activeIconColor: "text-metrics",
to: v3DashboardsLandingPath(organization, project, environment),
dataAction: "dashboards-landing",
action: (
<CreateDashboardButton
organization={organization}
project={project}
environment={environment}
isCollapsed={isCollapsed}
/>
),
after: (
<DashboardList
organization={organization}
project={project}
environment={environment}
isCollapsed={isCollapsed}
user={user}
/>
),
},
]
: []),
],
});
}
Expand Down
27 changes: 24 additions & 3 deletions apps/webapp/app/components/primitives/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export type SearchInputProps = {
/** Additional URL params to reset when searching or clearing (e.g. pagination). Defaults to ["cursor", "direction"]. */
resetParams?: string[];
autoFocus?: boolean;
minLength?: number;
/** Normalize the submitted value before applying minLength validation. */
normalizeForValidation?: (value: string) => string;
/**
* Controlled value. When provided alongside `onValueChange`, the input
* skips URL params entirely and acts as a controlled component — useful
Expand All @@ -34,6 +37,8 @@ export function SearchInput({
paramName = "search",
resetParams = ["cursor", "direction"],
autoFocus,
minLength,
normalizeForValidation,
value: controlledValue,
onValueChange,
}: SearchInputProps) {
Expand Down Expand Up @@ -70,20 +75,33 @@ export function SearchInput({
}, [isControlled, controlledValue, value, isFocused, paramName]);

const updateText = (next: string) => {
inputRef.current?.setCustomValidity("");
setText(next);
if (isControlled) {
onValueChange?.(next);
}
};

const handleSubmit = () => {
const trimmedText = text.trim();
const validationText = normalizeForValidation?.(trimmedText) ?? trimmedText;
if (
minLength !== undefined &&
trimmedText.length > 0 &&
[...validationText].length < minLength
) {
inputRef.current?.setCustomValidity(`Enter at least ${minLength} characters`);
inputRef.current?.reportValidity();
return;
}
Comment thread
carderne marked this conversation as resolved.
Comment thread
carderne marked this conversation as resolved.
inputRef.current?.setCustomValidity("");
if (isControlled) {
// Live updates already fired through onValueChange; submit is a no-op.
return;
}
const resetValues = Object.fromEntries(resetParams.map((p) => [p, undefined]));
if (text.trim()) {
replace({ [paramName]: text.trim(), ...resetValues });
if (trimmedText) {
replace({ [paramName]: trimmedText, ...resetValues });
} else {
del([paramName, ...resetParams]);
}
Expand Down Expand Up @@ -116,7 +134,10 @@ export function SearchInput({
variant="secondary-small"
placeholder={placeholder}
value={text}
onChange={(e) => updateText(e.target.value)}
onChange={(e) => {
e.currentTarget.setCustomValidity("");
updateText(e.target.value);
}}
fullWidth
autoFocus={autoFocus}
className={cn("", isFocused && "placeholder:text-text-dimmed/70")}
Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PassThrough } from "stream";
import { initMollifierDrainerWorker } from "~/v3/mollifierDrainerWorker.server";
import { initMollifierStaleSweepWorker } from "~/v3/mollifierStaleSweepWorker.server";
import { initBillingLimitWorker } from "~/v3/billingLimitWorker.server";
import { initLogsSearchProjectorWorker } from "~/v3/logsSearchProjectorWorker.server";
import { initQueueMetricsConsumer, initQueueMetricsEmitter } from "~/v3/queueMetrics.server";
import { bootstrap } from "./bootstrap";
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
Expand Down Expand Up @@ -277,6 +278,7 @@ export const handleError = wrapHandleErrorWithSentry((error, { request }) => {
initMollifierDrainerWorker();
initMollifierStaleSweepWorker();
initBillingLimitWorker();
initLogsSearchProjectorWorker();
initQueueMetricsEmitter();
initQueueMetricsConsumer();

Expand Down
37 changes: 21 additions & 16 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2094,20 +2094,28 @@ const EnvironmentSchema = z
.nonnegative()
.optional(),

// Logs list pagination tuning (page sizing + recent-first probe windows).
// Scheduled logs-search projection. Disabled by default. LOGS_CLICKHOUSE_URL, or the
// CLICKHOUSE_URL fallback, must reach both source and destination tables and allow writes.
LOGS_SEARCH_PROJECTOR_ENABLED: BoolEnv.default(false),
LOGS_SEARCH_PROJECTOR_PREVIEW_ENABLED: BoolEnv.default(false),
LOGS_SEARCH_PROJECTOR_MAX_WINDOWS_PER_TICK: z.coerce.number().int().min(1).max(20).default(5),
LOGS_SEARCH_PROJECTOR_MAX_EXECUTION_TIME_SECONDS: z.coerce
.number()
.int()
.min(1)
.max(300)
.default(120),
LOGS_SEARCH_PROJECTOR_MAX_ROWS_TO_READ: z.coerce.number().int().positive().default(10_000_000),
LOGS_SEARCH_PROJECTOR_MAX_MEMORY_USAGE: z.coerce
.number()
.int()
.positive()
.default(1_500_000_000),
LOGS_SEARCH_PROJECTOR_MAX_THREADS: z.coerce.number().int().min(1).max(8).default(2),

// Logs list pagination tuning.
LOGS_LIST_DEFAULT_PAGE_SIZE: z.coerce.number().int().positive().default(50),
LOGS_LIST_MAX_PAGE_SIZE: z.coerce.number().int().positive().default(100),
// Days back from the page ceiling to probe before widening to the full requested window,
// comma-separated. Empty disables narrowing (a single full-window query).
LOGS_LIST_RECENT_FIRST_PROBE_DAYS: z
.string()
.default("1,7")
.transform((s) =>
s
.split(",")
.map((v) => Number(v.trim()))
.filter((n) => Number.isFinite(n) && n > 0)
),

// Query feature flag
QUERY_FEATURE_ENABLED: z.string().default("1"),
Expand All @@ -2116,10 +2124,7 @@ const EnvironmentSchema = z
AI_FEATURES_ENABLED: z.string().default("0"),

// Logs page ClickHouse URL (for logs queries)
LOGS_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),
LOGS_CLICKHOUSE_URL: z.string().optional(),

// Query page ClickHouse limits (for TSQL queries)
QUERY_CLICKHOUSE_URL: z
Expand Down
Loading
Loading