Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -823,7 +823,7 @@ export function SideMenu({
});
}

if (isAdmin || featureFlags.hasQueryAccess) {
if (isAdmin || featureFlags.hasQueryAccess || featureFlags.hasLogsPageAccess) {
staticSections.push({
id: "metrics",
title: "Observability",
Expand All @@ -841,55 +841,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.
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 @@ -265,6 +266,7 @@ export const handleError = wrapHandleErrorWithSentry((error, { request }) => {
initMollifierDrainerWorker();
initMollifierStaleSweepWorker();
initBillingLimitWorker();
initLogsSearchProjectorWorker();
initQueueMetricsEmitter();
initQueueMetricsConsumer();

Expand Down
52 changes: 40 additions & 12 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1925,20 +1925,48 @@ const EnvironmentSchema = z
.nonnegative()
.optional(),

// Logs list pagination tuning (page sizing + recent-first probe windows).
// Keep reads on v1 until the scheduled v2 projector has enough history.
LOGS_SEARCH_TABLE_VERSION: z.enum(["v1", "v2"]).default("v1"),

// Scheduled logs-search projection. Disabled by default. The writer URL must reach both the
// task_events_v2 source and task_events_search_v2 destination tables.
LOGS_SEARCH_PROJECTOR_ENABLED: BoolEnv.default(false),
LOGS_SEARCH_PROJECTOR_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.EVENTS_CLICKHOUSE_URL ?? process.env.CLICKHOUSE_URL),
LOGS_SEARCH_PROJECTOR_SAFETY_DELAY_SECONDS: z.coerce
.number()
.int()
.min(60)
.max(3600)
.default(120),
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_SEARCH_PROJECTOR_BACKFILL_ENABLED: BoolEnv.default(false),
LOGS_SEARCH_PROJECTOR_MAX_BACKFILL_RANGE_DAYS: z.coerce
.number()
.int()
.min(1)
.max(90)
.default(7),
LOGS_SEARCH_PROJECTOR_MAX_BACKFILL_AGE_DAYS: z.coerce.number().int().min(1).max(90).default(90),

// 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 Down
Loading
Loading