Skip to content
Open
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
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
40 changes: 24 additions & 16 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2094,20 +2094,31 @@ 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. 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 +2127,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