-
- setActiveTab("details")}
- shortcut={{ key: "d" }}
- >
- Details
-
- setActiveTab("run")}
- shortcut={{ key: "r" }}
- >
- Run
-
-
-
-
- View full run
-
-
+
+ {getDisplayMessage(log)}
+
-
- {/* Content */}
-
- {activeTab === "details" && (
-
- )}
- {activeTab === "run" && (
-
- )}
+
+
);
}
-function DetailsTab({ log, runPath, searchTerm }: { log: LogEntry; runPath: string; searchTerm?: string }) {
- const logWithExtras = log as LogEntry & {
+function DetailsTab({
+ log,
+ runPath,
+ runStatus,
+ searchTerm,
+}: {
+ log: LogEntry & {
attributes?: LogAttributes;
};
-
-
+ runPath: string;
+ runStatus?: TaskRunStatus;
+ searchTerm?: string;
+}) {
let beautifiedAttributes: string | null = null;
- if (logWithExtras.attributes) {
- beautifiedAttributes = JSON.stringify(logWithExtras.attributes, null, 2);
+ if (log.attributes) {
+ beautifiedAttributes = JSON.stringify(log.attributes, null, 2);
beautifiedAttributes = formatStringJSON(beautifiedAttributes);
}
const showAttributes = beautifiedAttributes && beautifiedAttributes !== "{}";
- // Determine message to show
- let message = log.message ?? "";
- if (log.level === "ERROR") {
- const maybeErrorMessage = logWithExtras.attributes?.error?.message;
- if (typeof maybeErrorMessage === "string" && maybeErrorMessage.length > 0) {
- message = maybeErrorMessage;
- }
- }
+ const message = getDisplayMessage(log);
return (
<>
- {/* Time */}
-
-
- {/* Message */}
-
-
- {/* Attributes - only available in full log detail */}
- {showAttributes && beautifiedAttributes && (
-
- )}
- >
- );
-}
-
-function RunTab({ log, runPath }: { log: LogEntry; runPath: string }) {
- const organization = useOrganization();
- const project = useProject();
- const environment = useEnvironment();
- const fetcher = useTypedFetcher
();
-
- // Fetch run details when tab is active
- useEffect(() => {
- if (!log.runId) return;
-
- fetcher.load(
- `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/logs/${encodeURIComponent(log.id)}/run?runId=${encodeURIComponent(log.runId)}`
- );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [organization.slug, project.slug, environment.slug, log.id, log.runId]);
-
- const isLoading = fetcher.state === "loading";
- const runData = fetcher.data?.run;
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (!runData) {
- return (
-
-
Run not found in database.
-
- );
- }
-
- return (
-
Run ID
-
-
-
-
-
- Status
-
- }
- content={descriptionForTaskRunStatus(runData.status as TaskRunStatus)}
- disableHoverableContent
- />
-
-
-
-
- Task
-
-
+
+
+ View full run
+
- {runData.rootRun && (
+ {runStatus && (
- Root and parent run
+ Status
- }
+ content={descriptionForTaskRunStatus(runStatus)}
+ disableHoverableContent
+ className="mt-1"
/>
)}
- {runData.batch && (
-
- Batch
-
-
-
-
- )}
-
-
- Version
-
- {runData.version ? (
- environment.type === "DEVELOPMENT" ? (
-
- ) : (
-
-
-
- }
- content={"Jump to deployment"}
- />
- )
- ) : (
-
- Never started
-
-
- )}
-
-
-
-
- Test run
-
- {runData.isTest ? : "–"}
-
-
-
-
- Environment
-
-
-
-
-
- Queue
+ Task
- Name: {runData.queue}
- Concurrency key: {runData.concurrencyKey ? runData.concurrencyKey : "–"}
-
-
-
- {runData.tags && runData.tags.length > 0 && (
-
- Tags
-
-
- {runData.tags.map((tag: string) => (
-
- ))}
-
-
-
- )}
-
-
- Machine
-
- {runData.machinePreset ? (
-
- ) : (
- "–"
- )}
+
- Run invocation cost
+ Level
- {runData.baseCostInCents > 0
- ? formatCurrencyAccurate(runData.baseCostInCents / 100)
- : "–"}
+
- Compute cost
+ Timestamp
- {runData.costInCents > 0 ? formatCurrencyAccurate(runData.costInCents / 100) : "–"}
+
+
-
- Total cost
-
- {runData.costInCents > 0 || runData.baseCostInCents > 0
- ? formatCurrencyAccurate((runData.baseCostInCents + runData.costInCents) / 100)
- : "–"}
-
-
+ {/* Message */}
+
-
- Usage duration
-
- {runData.usageDurationMs > 0
- ? formatDurationMilliseconds(runData.usageDurationMs, { style: "short" })
- : "–"}
-
-
-
-
+ {/* Attributes - only available in full log detail */}
+ {showAttributes && beautifiedAttributes && (
+
+ )}
+ >
);
}
-
diff --git a/apps/webapp/app/components/logs/LogLevel.tsx b/apps/webapp/app/components/logs/LogLevel.tsx
new file mode 100644
index 00000000000..3c2c38301ff
--- /dev/null
+++ b/apps/webapp/app/components/logs/LogLevel.tsx
@@ -0,0 +1,16 @@
+import { cn } from "~/utils/cn";
+import { getLevelColor } from "~/utils/logUtils";
+import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
+
+export function LogLevel({ level }: { level: LogEntry["level"] }) {
+ return (
+
+ {level}
+
+ );
+}
diff --git a/apps/webapp/app/components/logs/LogsLevelFilter.tsx b/apps/webapp/app/components/logs/LogsLevelFilter.tsx
index 8c2abf64f25..947bef88fcc 100644
--- a/apps/webapp/app/components/logs/LogsLevelFilter.tsx
+++ b/apps/webapp/app/components/logs/LogsLevelFilter.tsx
@@ -16,6 +16,7 @@ import type { LogLevel } from "~/presenters/v3/LogsListPresenter.server";
import { cn } from "~/utils/cn";
const allLogLevels: { level: LogLevel; label: string; color: string }[] = [
+ { level: "TRACE", label: "Trace", color: "text-purple-400" },
{ level: "INFO", label: "Info", color: "text-blue-400" },
{ level: "WARN", label: "Warning", color: "text-warning" },
{ level: "ERROR", label: "Error", color: "text-error" },
@@ -33,6 +34,8 @@ function getLevelBadgeColor(level: LogLevel): string {
return "text-error bg-error/10 border-error/20";
case "WARN":
return "text-warning bg-warning/10 border-warning/20";
+ case "TRACE":
+ return "text-purple-400 bg-purple-500/10 border-purple-500/20";
case "DEBUG":
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
case "INFO":
diff --git a/apps/webapp/app/components/logs/LogsSearchInput.tsx b/apps/webapp/app/components/logs/LogsSearchInput.tsx
index fd539f66ae2..58316cead88 100644
--- a/apps/webapp/app/components/logs/LogsSearchInput.tsx
+++ b/apps/webapp/app/components/logs/LogsSearchInput.tsx
@@ -1,55 +1,49 @@
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid";
-import { useNavigate } from "@remix-run/react";
import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { Input } from "~/components/primitives/Input";
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import { cn } from "~/utils/cn";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
+import { useSearchParams } from "~/hooks/useSearchParam";
export function LogsSearchInput() {
const location = useOptimisticLocation();
- const navigate = useNavigate();
const inputRef = useRef(null);
+ const { value, replace, del } = useSearchParams();
+
// Get initial search value from URL
- const searchParams = new URLSearchParams(location.search);
- const initialSearch = searchParams.get("search") ?? "";
+ const initialSearch = value("search") ?? "";
const [text, setText] = useState(initialSearch);
const [isFocused, setIsFocused] = useState(false);
// Update text when URL search param changes (only when not focused to avoid overwriting user input)
useEffect(() => {
- const params = new URLSearchParams(location.search);
- const urlSearch = params.get("search") ?? "";
+ const urlSearch = value("search") ?? "";
if (urlSearch !== text && !isFocused) {
setText(urlSearch);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [location.search]);
+ }, [value, text, isFocused]);
const handleSubmit = useCallback(() => {
- const params = new URLSearchParams(location.search);
if (text.trim()) {
- params.set("search", text.trim());
+ replace({ search: text.trim() });
} else {
- params.delete("search");
+ del("search");
}
- // Reset cursor when searching
- params.delete("cursor");
- params.delete("direction");
- navigate(`${location.pathname}?${params.toString()}`, { replace: true });
- }, [text, location.pathname, location.search, navigate]);
+ }, [text, replace, del]);
- const handleClear = useCallback(() => {
- setText("");
- const params = new URLSearchParams(location.search);
- params.delete("search");
- params.delete("cursor");
- params.delete("direction");
- navigate(`${location.pathname}?${params.toString()}`, { replace: true });
- }, [location.pathname, location.search, navigate]);
+ const handleClear = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setText("");
+ del(["search", "cursor", "direction"]);
+ },
+ [del]
+ );
return (
@@ -71,7 +65,7 @@ export function LogsSearchInput() {
value={text}
onChange={(e) => setText(e.target.value)}
fullWidth
- className={cn(isFocused && "placeholder:text-text-dimmed/70")}
+ className={cn("", isFocused && "placeholder:text-text-dimmed/70")}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -86,22 +80,21 @@ export function LogsSearchInput() {
icon={
}
accessory={
text.length > 0 ? (
-
+
+
+
+
+
+
) : undefined
}
/>
-
- {text.length > 0 && (
-
-
-
- )}
);
}
diff --git a/apps/webapp/app/components/logs/LogsTable.tsx b/apps/webapp/app/components/logs/LogsTable.tsx
index 2f6894a03e6..0aedc4d706d 100644
--- a/apps/webapp/app/components/logs/LogsTable.tsx
+++ b/apps/webapp/app/components/logs/LogsTable.tsx
@@ -2,16 +2,17 @@ import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/20/so
import { Link } from "@remix-run/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
-import { Button } from "~/components/primitives/Buttons";
+import { Button, LinkButton } from "~/components/primitives/Buttons";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
-import { getLevelColor, highlightSearchText } from "~/utils/logUtils";
+import { highlightSearchText } from "~/utils/logUtils";
import { v3RunSpanPath } from "~/utils/pathBuilder";
import { DateTimeAccurate } from "../primitives/DateTime";
import { Paragraph } from "../primitives/Paragraph";
import { Spinner } from "../primitives/Spinner";
+import { LogLevel } from "./LogLevel";
import { TruncatedCopyableValue } from "../primitives/TruncatedCopyableValue";
import { LogLevelTooltipInfo } from "~/components/LogLevelTooltipInfo";
import {
@@ -48,14 +49,14 @@ function getLevelBoxShadow(level: LogEntry["level"]): string {
return "inset 2px 0 0 0 rgb(234, 179, 8)";
case "INFO":
return "inset 2px 0 0 0 rgb(59, 130, 246)";
+ case "TRACE":
+ return "inset 2px 0 0 0 rgb(168, 85, 247)";
case "DEBUG":
default:
return "none";
}
}
-
-
export function LogsTable({
logs,
searchTerm,
@@ -162,7 +163,7 @@ export function LogsTable({
boxShadow: getLevelBoxShadow(log.level),
}}
>
-
+
@@ -171,14 +172,7 @@ export function LogsTable({
{log.taskIdentifier}
-
- {log.level}
-
+
@@ -188,11 +182,13 @@ export function LogsTable({
-
- View run
-
-
+
+ View run
+
}
/>
@@ -233,11 +229,7 @@ function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?:
No logs match your filters. Try refreshing or modifying your filters.
-
+
Refresh
diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx
index 241bb8aea2b..adedf7b589d 100644
--- a/apps/webapp/app/components/navigation/SideMenu.tsx
+++ b/apps/webapp/app/components/navigation/SideMenu.tsx
@@ -433,18 +433,7 @@ export function SideMenu({
data-action="deployments"
isCollapsed={isCollapsed}
/>
- {(user.admin || user.isImpersonating || featureFlags.hasLogsPageAccess) && (
- }
- isCollapsed={isCollapsed}
- />
- )}
+
+ {(user.admin || user.isImpersonating || featureFlags.hasLogsPageAccess) && (
+ }
+ isCollapsed={isCollapsed}
+ />
+ )}
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))} ;
+ return (
+
+ {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
+
+ );
};
// Helper function to check if two dates are on the same day
@@ -270,14 +274,18 @@ const DateTimeAccurateInner = ({
return hideDate
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
: realPrevDate
- ? isSameDay(realDate, realPrevDate)
- ? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
- : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
- : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
+ ? isSameDay(realDate, realPrevDate)
+ ? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
+ : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
+ : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
}, [realDate, displayTimeZone, locales, hour12, hideDate, previousDate]);
if (!showTooltip)
- return {formattedDateTime.replace(/\s/g, String.fromCharCode(32))} ;
+ return (
+
+ {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
+
+ );
const tooltipContent = (
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}}
+ button={
+
+ {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
+
+ }
content={tooltipContent}
side="right"
asChild={true}
@@ -326,9 +338,13 @@ function formatDateTimeAccurate(
locales: string[],
hour12: boolean = true
): string {
- const formattedDateTime = new Intl.DateTimeFormat(locales, {
+ const datePart = new Intl.DateTimeFormat(locales, {
month: "short",
day: "numeric",
+ timeZone,
+ }).format(date);
+
+ const timePart = new Intl.DateTimeFormat(locales, {
hour: "numeric",
minute: "numeric",
second: "numeric",
@@ -338,7 +354,7 @@ function formatDateTimeAccurate(
hour12,
}).format(date);
- return formattedDateTime;
+ return `${datePart} ${timePart}`;
}
export const DateTimeShort = ({ date, hour12 = true }: DateTimeProps) => {
@@ -347,7 +363,11 @@ export const DateTimeShort = ({ date, hour12 = true }: DateTimeProps) => {
const realDate = typeof date === "string" ? new Date(date) : date;
const formattedDateTime = formatDateTimeShort(realDate, userTimeZone, locales, hour12);
- return {formattedDateTime.replace(/\s/g, String.fromCharCode(32))} ;
+ return (
+
+ {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
+
+ );
};
function formatDateTimeShort(
diff --git a/apps/webapp/app/components/runs/v3/PacketDisplay.tsx b/apps/webapp/app/components/runs/v3/PacketDisplay.tsx
index 2d6b6e65be6..82deae8a1be 100644
--- a/apps/webapp/app/components/runs/v3/PacketDisplay.tsx
+++ b/apps/webapp/app/components/runs/v3/PacketDisplay.tsx
@@ -12,11 +12,13 @@ export function PacketDisplay({
dataType,
title,
searchTerm,
+ wrap,
}: {
data: string;
dataType: string;
title: string;
searchTerm?: string;
+ wrap?: boolean;
}) {
switch (dataType) {
case "application/store": {
@@ -54,6 +56,7 @@ export function PacketDisplay({
showLineNumbers={false}
showTextWrapping
searchTerm={searchTerm}
+ wrap={wrap}
/>
);
}
@@ -67,6 +70,7 @@ export function PacketDisplay({
showLineNumbers={false}
showTextWrapping
searchTerm={searchTerm}
+ wrap={wrap}
/>
);
}
diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx
index 7a5c4fdba93..5538c628178 100644
--- a/apps/webapp/app/components/runs/v3/SharedFilters.tsx
+++ b/apps/webapp/app/components/runs/v3/SharedFilters.tsx
@@ -219,7 +219,7 @@ export function timeFilterFromTo(props: {
from?: string | number;
to?: string | number;
defaultPeriod: string;
-}): { from: Date; to: Date } {
+}): { from: Date; to: Date; isDefault: boolean } {
const time = timeFilters(props);
const periodMs = time.period ? parse(time.period) : undefined;
@@ -228,6 +228,7 @@ export function timeFilterFromTo(props: {
return {
from: new Date(Date.now() - periodMs),
to: new Date(),
+ isDefault: time.isDefault,
};
}
@@ -235,6 +236,7 @@ export function timeFilterFromTo(props: {
return {
from: time.from,
to: time.to,
+ isDefault: time.isDefault,
};
}
@@ -242,6 +244,7 @@ export function timeFilterFromTo(props: {
return {
from: time.from,
to: new Date(),
+ isDefault: time.isDefault,
};
}
@@ -249,6 +252,7 @@ export function timeFilterFromTo(props: {
return {
from: new Date(Date.now() - defaultPeriodMs),
to: time.to ?? new Date(),
+ isDefault: time.isDefault,
};
}
diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts
index 5a321c58b64..6d3e6fbe3d1 100644
--- a/apps/webapp/app/env.server.ts
+++ b/apps/webapp/app/env.server.ts
@@ -1180,7 +1180,7 @@ const EnvironmentSchema = z
CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"),
CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
- // Logs List Query Settings (for paginated log views)
+ // Logs Query Settings
CLICKHOUSE_LOGS_LIST_MAX_MEMORY_USAGE: z.coerce.number().int().default(1_000_000_000),
CLICKHOUSE_LOGS_LIST_MAX_BYTES_BEFORE_EXTERNAL_SORT: z.coerce
.number()
@@ -1190,14 +1190,15 @@ const EnvironmentSchema = z
CLICKHOUSE_LOGS_LIST_MAX_ROWS_TO_READ: z.coerce.number().int().default(10_000_000),
CLICKHOUSE_LOGS_LIST_MAX_EXECUTION_TIME: z.coerce.number().int().default(120),
- // Logs Detail Query Settings (for single log views)
- CLICKHOUSE_LOGS_DETAIL_MAX_MEMORY_USAGE: z.coerce.number().int().default(64_000_000),
- CLICKHOUSE_LOGS_DETAIL_MAX_THREADS: z.coerce.number().int().default(2),
- CLICKHOUSE_LOGS_DETAIL_MAX_EXECUTION_TIME: z.coerce.number().int().default(60),
-
// Query feature flag
QUERY_FEATURE_ENABLED: z.string().default("1"),
+ // Logs page ClickHouse URL (for logs queries)
+ LOGS_CLICKHOUSE_URL: z
+ .string()
+ .optional()
+ .transform((v) => v ?? process.env.CLICKHOUSE_URL),
+
// Query page ClickHouse limits (for TSQL queries)
QUERY_CLICKHOUSE_URL: z
.string()
diff --git a/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts b/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts
index 545bca5cce7..8a3bf692b5b 100644
--- a/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/LogsListPresenter.server.ts
@@ -1,13 +1,11 @@
import { z } from "zod";
-import { type ClickHouse } from "@internal/clickhouse";
-import {
- type PrismaClientOrTransaction,
-} from "@trigger.dev/database";
+import { type ClickHouse, type WhereCondition } from "@internal/clickhouse";
+import { type PrismaClientOrTransaction } from "@trigger.dev/database";
import { EVENT_STORE_TYPES, getConfiguredEventRepository } from "~/v3/eventRepository/index.server";
import parseDuration from "parse-duration";
import { type Direction } from "~/components/ListPagination";
-import { timeFilters } from "~/components/runs/v3/SharedFilters";
+import { timeFilterFromTo, timeFilters } from "~/components/runs/v3/SharedFilters";
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
import { getAllTaskIdentifiers } from "~/models/task.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
@@ -28,14 +26,9 @@ type ErrorAttributes = {
};
function escapeClickHouseString(val: string): string {
- return val
- .replace(/\\/g, "\\\\")
- .replace(/\//g, "\\/")
- .replace(/%/g, "\\%")
- .replace(/_/g, "\\_");
+ return val.replace(/\\/g, "\\\\").replace(/\//g, "\\/").replace(/%/g, "\\%").replace(/_/g, "\\_");
}
-
export type LogsListOptions = {
userId?: string;
projectId: string;
@@ -115,10 +108,12 @@ function decodeCursor(cursor: string): LogCursor | null {
// Convert display level to ClickHouse kinds and statuses
function levelToKindsAndStatuses(level: LogLevel): { kinds?: string[]; statuses?: string[] } {
switch (level) {
+ case "TRACE":
+ return { kinds: ["SPAN"] };
case "DEBUG":
return { kinds: ["LOG_DEBUG"] };
case "INFO":
- return { kinds: ["LOG_INFO", "LOG_LOG", "SPAN"] };
+ return { kinds: ["LOG_INFO", "LOG_LOG"] };
case "WARN":
return { kinds: ["LOG_WARN"] };
case "ERROR":
@@ -153,24 +148,16 @@ export class LogsListPresenter extends BasePresenter {
retentionLimitDays,
}: LogsListOptions
) {
- const time = timeFilters({
+ const time = timeFilterFromTo({
period,
from,
to,
- defaultPeriod,
+ defaultPeriod: defaultPeriod ?? "1h",
});
let effectiveFrom = time.from;
let effectiveTo = time.to;
- if (!effectiveFrom && !effectiveTo && time.period) {
- const periodMs = parseDuration(time.period);
- if (periodMs) {
- effectiveFrom = new Date(Date.now() - periodMs);
- effectiveTo = new Date();
- }
- }
-
// Apply retention limit if provided
let wasClampedByRetention = false;
if (retentionLimitDays !== undefined && effectiveFrom) {
@@ -236,6 +223,11 @@ export class LogsListPresenter extends BasePresenter {
const queryBuilder = this.clickhouse.taskEventsSearch.logsListQueryBuilder();
+ // This should be removed once we clear the old inserts, 30 DAYS, the materialized view excludes events without trace_id)
+ queryBuilder.where("trace_id != ''", {
+ environmentId,
+ });
+
queryBuilder.where("environment_id = {environmentId: String}", {
environmentId,
});
@@ -245,11 +237,10 @@ export class LogsListPresenter extends BasePresenter {
});
queryBuilder.where("project_id = {projectId: String}", { projectId });
-
if (effectiveFrom) {
- queryBuilder.where("triggered_timestamp >= {triggeredAtStart: DateTime64(3)}", {
- triggeredAtStart: convertDateToClickhouseDateTime(effectiveFrom),
- });
+ queryBuilder.where("triggered_timestamp >= {triggeredAtStart: DateTime64(3)}", {
+ triggeredAtStart: convertDateToClickhouseDateTime(effectiveFrom),
+ });
}
if (effectiveTo) {
@@ -278,50 +269,43 @@ export class LogsListPresenter extends BasePresenter {
queryBuilder.where(
"(lower(message) like {searchPattern: String} OR lower(attributes_text) like {searchPattern: String})",
{
- searchPattern: `%${searchTerm}%`
+ searchPattern: `%${searchTerm}%`,
}
);
}
if (levels && levels.length > 0) {
- const conditions: string[] = [];
- const params: Record = {};
+ const conditions: WhereCondition[] = [];
- for (const level of levels) {
- const filter = levelToKindsAndStatuses(level);
- const levelConditions: string[] = [];
+ for (let i = 0; i < levels.length; i++) {
+ const filter = levelToKindsAndStatuses(levels[i]);
if (filter.kinds && filter.kinds.length > 0) {
- const kindsKey = `kinds_${level}`;
- let kindCondition = `kind IN {${kindsKey}: Array(String)}`;
-
-
- kindCondition += ` AND status NOT IN {excluded_statuses: Array(String)}`;
- params["excluded_statuses"] = ["ERROR", "CANCELLED"];
-
-
- levelConditions.push(kindCondition);
- params[kindsKey] = filter.kinds;
+ conditions.push({
+ clause: `kind IN {kinds_${i}: Array(String)} AND status NOT IN {excluded_statuses: Array(String)}`,
+ params: {
+ [`kinds_${i}`]: filter.kinds,
+ excluded_statuses: ["ERROR", "CANCELLED"],
+ },
+ });
}
if (filter.statuses && filter.statuses.length > 0) {
- const statusesKey = `statuses_${level}`;
- levelConditions.push(`status IN {${statusesKey}: Array(String)}`);
- params[statusesKey] = filter.statuses;
- }
-
- if (levelConditions.length > 0) {
- conditions.push(`(${levelConditions.join(" OR ")})`);
+ conditions.push({
+ clause: `status IN {statuses_${i}: Array(String)}`,
+ params: { [`statuses_${i}`]: filter.statuses },
+ });
}
}
- if (conditions.length > 0) {
- queryBuilder.where(`(${conditions.join(" OR ")})`, params);
- }
+ queryBuilder.whereOr(conditions);
}
- // Cursor pagination using explicit lexicographic comparison
- // Must mirror the ORDER BY columns: (organization_id, environment_id, triggered_timestamp, trace_id)
+ // Cursor-based pagination using lexicographic comparison on (triggered_timestamp, trace_id).
+ // Since ORDER BY is DESC, "next page" means rows that sort *after* the cursor, i.e. less-than.
+ // The OR handles the tiebreaker: rows with an earlier timestamp always qualify, and rows
+ // with the *same* timestamp only qualify if their trace_id is also smaller.
+ // Equivalent to: WHERE (triggered_timestamp, trace_id) < (cursor.triggered_timestamp, cursor.trace_id)
const decodedCursor = cursor ? decodeCursor(cursor) : null;
if (decodedCursor) {
queryBuilder.where(
@@ -423,10 +407,13 @@ export class LogsListPresenter extends BasePresenter {
hasFilters,
hasAnyLogs: transformedLogs.length > 0,
searchTerm: search,
- retention: retentionLimitDays !== undefined ? {
- limitDays: retentionLimitDays,
- wasClamped: wasClampedByRetention,
- } : undefined,
+ retention:
+ retentionLimitDays !== undefined
+ ? {
+ limitDays: retentionLimitDays,
+ wasClamped: wasClampedByRetention,
+ }
+ : undefined,
};
}
}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx
index 9c2eccc8ea0..325df8b386b 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx
@@ -16,7 +16,7 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { LogsListPresenter, LogEntry } from "~/presenters/v3/LogsListPresenter.server";
import type { LogLevel } from "~/utils/logUtils";
import { $replica, prisma } from "~/db.server";
-import { clickhouseClient } from "~/services/clickhouseInstance.server";
+import { logsClickhouseClient } from "~/services/clickhouseInstance.server";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react";
@@ -40,7 +40,7 @@ import { Button } from "~/components/primitives/Buttons";
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags.server";
// Valid log levels for filtering
-const validLevels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"];
+const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
function parseLevelsFromUrl(url: URL): LogLevel[] | undefined {
const levelParams = url.searchParams.getAll("levels").filter((v) => v.length > 0);
@@ -134,7 +134,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const plan = await getCurrentPlan(project.organizationId);
const retentionLimitDays = plan?.v3Subscription?.plan?.limits.logRetentionDays.number ?? 30;
- const presenter = new LogsListPresenter($replica, clickhouseClient);
+ const presenter = new LogsListPresenter($replica, logsClickhouseClient);
const listPromise = presenter
.call(project.organizationId, environment.id, {
@@ -322,7 +322,10 @@ function LogsList({
const [nextCursor, setNextCursor] = useState(list.pagination.next);
// Selected log state - managed locally to avoid triggering navigation
- const [selectedLogId, setSelectedLogId] = useState();
+ const [selectedLogId, setSelectedLogId] = useState(() => {
+ const params = new URLSearchParams(location.search);
+ return params.get("log") ?? undefined;
+ });
// Track which filter state (search params) the current fetcher request corresponds to
const fetcherFilterStateRef = useRef(location.search);
@@ -333,8 +336,9 @@ function LogsList({
useEffect(() => {
setAccumulatedLogs([]);
setNextCursor(undefined);
- // Close side panel when filters change to avoid showing a log that's no longer visible
- setSelectedLogId(undefined);
+ // Preserve log selection from URL param, clear if not present
+ const params = new URLSearchParams(location.search);
+ setSelectedLogId(params.get("log") ?? undefined);
}, [location.search]);
// Populate accumulated logs when new data arrives
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.run.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.run.tsx
deleted file mode 100644
index 15986c305af..00000000000
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.run.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
-import { json } from "@remix-run/node";
-import { z } from "zod";
-import { MachinePresetName } from "@trigger.dev/core/v3";
-import { requireUserId } from "~/services/session.server";
-import { EnvironmentParamSchema } from "~/utils/pathBuilder";
-import { findProjectBySlug } from "~/models/project.server";
-import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
-import { $replica } from "~/db.server";
-import type { TaskRunStatus } from "@trigger.dev/database";
-
-// Valid TaskRunStatus values
-const VALID_TASK_RUN_STATUSES = [
- "PENDING",
- "QUEUED",
- "EXECUTING",
- "WAITING_FOR_EXECUTION",
- "WAITING",
- "COMPLETED_SUCCESSFULLY",
- "COMPLETED_WITH_ERRORS",
- "SYSTEM_FAILURE",
- "FAILURE",
- "CANCELED",
-] as const;
-
-// Schema for validating run context data
-export const RunContextSchema = z.object({
- id: z.string(),
- friendlyId: z.string(),
- taskIdentifier: z.string(),
- status: z.enum(VALID_TASK_RUN_STATUSES),
- createdAt: z.string().datetime(),
- startedAt: z.string().datetime().optional(),
- completedAt: z.string().datetime().optional(),
- isTest: z.boolean(),
- tags: z.array(z.string()),
- queue: z.string(),
- concurrencyKey: z.string().nullable(),
- usageDurationMs: z.number(),
- costInCents: z.number(),
- baseCostInCents: z.number(),
- machinePreset: MachinePresetName.nullable(),
- version: z.string().optional(),
- rootRun: z
- .object({
- friendlyId: z.string(),
- taskIdentifier: z.string(),
- })
- .nullable(),
- parentRun: z
- .object({
- friendlyId: z.string(),
- taskIdentifier: z.string(),
- })
- .nullable(),
- batch: z
- .object({
- friendlyId: z.string(),
- })
- .nullable(),
- schedule: z
- .object({
- friendlyId: z.string(),
- })
- .nullable(),
-});
-
-export type RunContext = z.infer;
-
-// Fetch run context for a log entry
-export const loader = async ({ request, params }: LoaderFunctionArgs) => {
- const userId = await requireUserId(request);
- const { projectParam, organizationSlug, envParam, logId } = {
- ...EnvironmentParamSchema.parse(params),
- logId: params.logId,
- };
-
- if (!logId) {
- throw new Response("Log ID is required", { status: 400 });
- }
-
- const project = await findProjectBySlug(organizationSlug, projectParam, userId);
- if (!project) {
- throw new Response("Project not found", { status: 404 });
- }
-
- const environment = await findEnvironmentBySlug(project.id, envParam, userId);
- if (!environment) {
- throw new Response("Environment not found", { status: 404 });
- }
-
- // Parse the logId to extract runId
- // Log ID format: traceId::spanId::runId::startTime (base64 encoded or plain)
- const url = new URL(request.url);
- const runId = url.searchParams.get("runId");
-
- if (!runId) {
- throw new Response("Run ID is required", { status: 400 });
- }
-
- // Fetch run details from Postgres
- const run = await $replica.taskRun.findFirst({
- select: {
- id: true,
- friendlyId: true,
- taskIdentifier: true,
- status: true,
- createdAt: true,
- startedAt: true,
- completedAt: true,
- isTest: true,
- runTags: true,
- queue: true,
- concurrencyKey: true,
- usageDurationMs: true,
- costInCents: true,
- baseCostInCents: true,
- machinePreset: true,
- scheduleId: true,
- lockedToVersion: {
- select: {
- version: true,
- },
- },
- rootTaskRun: {
- select: {
- friendlyId: true,
- taskIdentifier: true,
- },
- },
- parentTaskRun: {
- select: {
- friendlyId: true,
- taskIdentifier: true,
- },
- },
- batch: {
- select: {
- friendlyId: true,
- },
- },
- },
- where: {
- friendlyId: runId,
- runtimeEnvironmentId: environment.id,
- },
- });
-
- if (!run) {
- return json({ run: null });
- }
-
- // Fetch schedule if scheduleId exists
- let schedule: { friendlyId: string } | null = null;
- if (run.scheduleId) {
- const scheduleData = await $replica.taskSchedule.findFirst({
- select: { friendlyId: true },
- where: { id: run.scheduleId },
- });
- schedule = scheduleData;
- }
-
- const runData = {
- id: run.id,
- friendlyId: run.friendlyId,
- taskIdentifier: run.taskIdentifier,
- status: run.status,
- createdAt: run.createdAt.toISOString(),
- startedAt: run.startedAt?.toISOString(),
- completedAt: run.completedAt?.toISOString(),
- isTest: run.isTest,
- tags: run.runTags,
- queue: run.queue,
- concurrencyKey: run.concurrencyKey,
- usageDurationMs: run.usageDurationMs,
- costInCents: run.costInCents,
- baseCostInCents: run.baseCostInCents,
- machinePreset: run.machinePreset,
- version: run.lockedToVersion?.version,
- rootRun: run.rootTaskRun
- ? {
- friendlyId: run.rootTaskRun.friendlyId,
- taskIdentifier: run.rootTaskRun.taskIdentifier,
- }
- : null,
- parentRun: run.parentTaskRun
- ? {
- friendlyId: run.parentTaskRun.friendlyId,
- taskIdentifier: run.parentTaskRun.taskIdentifier,
- }
- : null,
- batch: run.batch ? { friendlyId: run.batch.friendlyId } : null,
- schedule: schedule,
- };
-
- // Validate the run data
- const validatedRun = RunContextSchema.parse(runData);
-
- return json({
- run: validatedRun,
- });
-};
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx
index f04e706d4e5..f862ced6b05 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId.tsx
@@ -1,13 +1,14 @@
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import { z } from "zod";
-import { clickhouseClient } from "~/services/clickhouseInstance.server";
+import { logsClickhouseClient } from "~/services/clickhouseInstance.server";
import { requireUserId } from "~/services/session.server";
import { LogDetailPresenter } from "~/presenters/v3/LogDetailPresenter.server";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { $replica } from "~/db.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
+import type { TaskRunStatus } from "@trigger.dev/database";
const LogIdParamsSchema = z.object({
organizationSlug: z.string(),
@@ -42,7 +43,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const [traceId, spanId, , startTime] = parts;
- const presenter = new LogDetailPresenter($replica, clickhouseClient);
+ const presenter = new LogDetailPresenter($replica, logsClickhouseClient);
let result;
try {
@@ -65,5 +66,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
throw new Response("Log not found", { status: 404 });
}
- return typedjson(result);
+ // Look up the run status from Postgres
+ let runStatus: TaskRunStatus | undefined;
+ if (result.runId) {
+ const run = await $replica.taskRun.findFirst({
+ select: { status: true },
+ where: {
+ friendlyId: result.runId,
+ runtimeEnvironmentId: environment.id,
+ },
+ });
+ runStatus = run?.status;
+ }
+
+ return typedjson({ ...result, runStatus });
};
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts
index cac4c0f7025..66ddebe4e2a 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts
@@ -6,11 +6,11 @@ import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { LogsListPresenter, type LogLevel, LogsListOptionsSchema } from "~/presenters/v3/LogsListPresenter.server";
import { $replica } from "~/db.server";
-import { clickhouseClient } from "~/services/clickhouseInstance.server";
+import { logsClickhouseClient } from "~/services/clickhouseInstance.server";
import { getCurrentPlan } from "~/services/platform.v3.server";
// Valid log levels for filtering
-const validLevels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"];
+const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
function parseLevelsFromUrl(url: URL): LogLevel[] | undefined {
const levelParams = url.searchParams.getAll("levels").filter((v) => v.length > 0);
@@ -69,7 +69,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
retentionLimitDays,
}) as any; // Validated by LogsListOptionsSchema at runtime
- const presenter = new LogsListPresenter($replica, clickhouseClient);
+ const presenter = new LogsListPresenter($replica, logsClickhouseClient);
const result = await presenter.call(project.organizationId, environment.id, options);
return json({
diff --git a/apps/webapp/app/services/clickhouseInstance.server.ts b/apps/webapp/app/services/clickhouseInstance.server.ts
index 7818129b421..f88b3baaaed 100644
--- a/apps/webapp/app/services/clickhouseInstance.server.ts
+++ b/apps/webapp/app/services/clickhouseInstance.server.ts
@@ -12,29 +12,6 @@ function initializeClickhouseClient() {
console.log(`🗃️ Clickhouse service enabled to host ${url.host}`);
- // Build logs query settings from environment variables
- const logsQuerySettings = {
- list: {
- max_memory_usage: env.CLICKHOUSE_LOGS_LIST_MAX_MEMORY_USAGE.toString(),
- max_bytes_before_external_sort:
- env.CLICKHOUSE_LOGS_LIST_MAX_BYTES_BEFORE_EXTERNAL_SORT.toString(),
- max_threads: env.CLICKHOUSE_LOGS_LIST_MAX_THREADS,
- ...(env.CLICKHOUSE_LOGS_LIST_MAX_ROWS_TO_READ && {
- max_rows_to_read: env.CLICKHOUSE_LOGS_LIST_MAX_ROWS_TO_READ.toString(),
- }),
- ...(env.CLICKHOUSE_LOGS_LIST_MAX_EXECUTION_TIME && {
- max_execution_time: env.CLICKHOUSE_LOGS_LIST_MAX_EXECUTION_TIME,
- }),
- },
- detail: {
- max_memory_usage: env.CLICKHOUSE_LOGS_DETAIL_MAX_MEMORY_USAGE.toString(),
- max_threads: env.CLICKHOUSE_LOGS_DETAIL_MAX_THREADS,
- ...(env.CLICKHOUSE_LOGS_DETAIL_MAX_EXECUTION_TIME && {
- max_execution_time: env.CLICKHOUSE_LOGS_DETAIL_MAX_EXECUTION_TIME,
- }),
- },
- };
-
const clickhouse = new ClickHouse({
url: url.toString(),
name: "clickhouse-instance",
@@ -47,12 +24,53 @@ function initializeClickhouseClient() {
request: true,
},
maxOpenConnections: env.CLICKHOUSE_MAX_OPEN_CONNECTIONS,
- logsQuerySettings,
});
return clickhouse;
}
+export const logsClickhouseClient = singleton(
+ "logsClickhouseClient",
+ initializeLogsClickhouseClient
+);
+
+function initializeLogsClickhouseClient() {
+ if (!env.LOGS_CLICKHOUSE_URL) {
+ throw new Error("LOGS_CLICKHOUSE_URL is not set");
+ }
+
+ const url = new URL(env.LOGS_CLICKHOUSE_URL);
+
+ // Remove secure param
+ url.searchParams.delete("secure");
+
+ return new ClickHouse({
+ url: url.toString(),
+ name: "logs-clickhouse",
+ keepAlive: {
+ enabled: env.CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
+ idleSocketTtl: env.CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
+ },
+ logLevel: env.CLICKHOUSE_LOG_LEVEL,
+ compression: {
+ request: true,
+ },
+ maxOpenConnections: env.CLICKHOUSE_MAX_OPEN_CONNECTIONS,
+ clickhouseSettings: {
+ max_memory_usage: env.CLICKHOUSE_LOGS_LIST_MAX_MEMORY_USAGE.toString(),
+ max_bytes_before_external_sort:
+ env.CLICKHOUSE_LOGS_LIST_MAX_BYTES_BEFORE_EXTERNAL_SORT.toString(),
+ max_threads: env.CLICKHOUSE_LOGS_LIST_MAX_THREADS,
+ ...(env.CLICKHOUSE_LOGS_LIST_MAX_ROWS_TO_READ && {
+ max_rows_to_read: env.CLICKHOUSE_LOGS_LIST_MAX_ROWS_TO_READ.toString(),
+ }),
+ ...(env.CLICKHOUSE_LOGS_LIST_MAX_EXECUTION_TIME && {
+ max_execution_time: env.CLICKHOUSE_LOGS_LIST_MAX_EXECUTION_TIME,
+ }),
+ },
+ });
+}
+
export const queryClickhouseClient = singleton(
"queryClickhouseClient",
initializeQueryClickhouseClient
diff --git a/apps/webapp/app/utils/logUtils.ts b/apps/webapp/app/utils/logUtils.ts
index cad9bbc9070..71ec44534b0 100644
--- a/apps/webapp/app/utils/logUtils.ts
+++ b/apps/webapp/app/utils/logUtils.ts
@@ -1,10 +1,10 @@
import { createElement, Fragment, type ReactNode } from "react";
import { z } from "zod";
-export const LogLevelSchema = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]);
+export const LogLevelSchema = z.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]);
export type LogLevel = z.infer;
-export const validLogLevels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR",];
+export const validLogLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
// Default styles for search highlighting
const DEFAULT_HIGHLIGHT_STYLES: React.CSSProperties = {
@@ -87,6 +87,7 @@ export function kindToLevel(kind: string, status: string): LogLevel {
case "LOG_LOG":
return "INFO"; // Changed from "LOG"
case "SPAN":
+ return "TRACE";
case "ANCESTOR_OVERRIDE":
case "SPAN_EVENT":
default:
@@ -101,6 +102,8 @@ export function getLevelColor(level: LogLevel): string {
return "text-error bg-error/10 border-error/20";
case "WARN":
return "text-warning bg-warning/10 border-warning/20";
+ case "TRACE":
+ return "text-purple-400 bg-purple-500/10 border-purple-500/20";
case "DEBUG":
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
case "INFO":
diff --git a/apps/webapp/tailwind.config.js b/apps/webapp/tailwind.config.js
index f17eb04c15b..b3d6e2b235f 100644
--- a/apps/webapp/tailwind.config.js
+++ b/apps/webapp/tailwind.config.js
@@ -166,7 +166,7 @@ const deployments = colors.green[500];
const concurrency = colors.amber[500];
const limits = colors.purple[500];
const regions = colors.green[500];
-const logs = colors.blue[500];
+const logs = colors.pink[500];
const tests = colors.lime[500];
const apiKeys = colors.amber[500];
const environmentVariables = colors.pink[500];
diff --git a/internal-packages/clickhouse/schema/017_update_materialized_conditions_to_task_events_search_v1.sql b/internal-packages/clickhouse/schema/017_update_materialized_conditions_to_task_events_search_v1.sql
new file mode 100644
index 00000000000..3f84cf555f2
--- /dev/null
+++ b/internal-packages/clickhouse/schema/017_update_materialized_conditions_to_task_events_search_v1.sql
@@ -0,0 +1,62 @@
+-- +goose Up
+-- We drop the existing MV and recreate it with the new filter condition
+DROP VIEW IF EXISTS trigger_dev.task_events_search_mv_v1;
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS trigger_dev.task_events_search_mv_v1
+TO trigger_dev.task_events_search_v1 AS
+SELECT
+ environment_id,
+ organization_id,
+ project_id,
+ trace_id,
+ span_id,
+ run_id,
+ task_identifier,
+ start_time,
+ inserted_at,
+ message,
+ kind,
+ status,
+ duration,
+ parent_span_id,
+ attributes_text,
+ fromUnixTimestamp64Nano(toUnixTimestamp64Nano(start_time) + toInt64(duration)) AS triggered_timestamp
+FROM trigger_dev.task_events_v2
+WHERE
+ trace_id != '' -- New condition added here
+ AND kind != 'DEBUG_EVENT'
+ AND status != 'PARTIAL'
+ AND NOT (kind = 'SPAN_EVENT' AND attributes_text = '{}')
+ AND kind != 'ANCESTOR_OVERRIDE'
+ AND message != 'trigger.dev/start';
+
+-- +goose Down
+-- In the down migration, we revert to the previous filter set
+DROP VIEW IF EXISTS trigger_dev.task_events_search_mv_v1;
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS trigger_dev.task_events_search_mv_v1
+TO trigger_dev.task_events_search_v1 AS
+SELECT
+ environment_id,
+ organization_id,
+ project_id,
+ trace_id,
+ span_id,
+ run_id,
+ task_identifier,
+ start_time,
+ inserted_at,
+ message,
+ kind,
+ status,
+ duration,
+ parent_span_id,
+ attributes_text,
+ fromUnixTimestamp64Nano(toUnixTimestamp64Nano(start_time) + toInt64(duration)) AS triggered_timestamp
+FROM trigger_dev.task_events_v2
+WHERE
+ kind != 'DEBUG_EVENT'
+ AND status != 'PARTIAL'
+ AND NOT (kind = 'SPAN_EVENT' AND attributes_text = '{}')
+ AND kind != 'ANCESTOR_OVERRIDE'
+ AND message != 'trigger.dev/start';
\ No newline at end of file
diff --git a/internal-packages/clickhouse/schema/018_drop_unused_task_events_v2_indexes.sql b/internal-packages/clickhouse/schema/018_drop_unused_task_events_v2_indexes.sql
new file mode 100644
index 00000000000..81e3dbab61c
--- /dev/null
+++ b/internal-packages/clickhouse/schema/018_drop_unused_task_events_v2_indexes.sql
@@ -0,0 +1,22 @@
+-- +goose Up
+
+-- These indexes are not used in any WHERE clause.
+-- idx_duration: duration is only written/read as a column, never filtered on.
+-- idx_attributes_text: search queries use the task_events_search_v1 table instead.
+ALTER TABLE trigger_dev.task_events_v2
+ DROP INDEX IF EXISTS idx_duration;
+
+ALTER TABLE trigger_dev.task_events_v2
+ DROP INDEX IF EXISTS idx_attributes_text;
+
+-- +goose Down
+
+ALTER TABLE trigger_dev.task_events_v2
+ ADD INDEX IF NOT EXISTS idx_duration duration
+ TYPE minmax
+ GRANULARITY 1;
+
+ALTER TABLE trigger_dev.task_events_v2
+ ADD INDEX IF NOT EXISTS idx_attributes_text attributes_text
+ TYPE tokenbf_v1(32768, 3, 0)
+ GRANULARITY 8;
diff --git a/internal-packages/clickhouse/src/client/queryBuilder.ts b/internal-packages/clickhouse/src/client/queryBuilder.ts
index 30aad98486c..e802fc11bf3 100644
--- a/internal-packages/clickhouse/src/client/queryBuilder.ts
+++ b/internal-packages/clickhouse/src/client/queryBuilder.ts
@@ -4,6 +4,11 @@ import { ClickHouseSettings } from "@clickhouse/client";
export type QueryParamValue = string | number | boolean | Array | null;
export type QueryParams = Record;
+export type WhereCondition = {
+ clause: string;
+ params?: QueryParams;
+};
+
export class ClickhouseQueryBuilder {
private name: string;
private baseQuery: string;
@@ -45,6 +50,20 @@ export class ClickhouseQueryBuilder {
return this;
}
+ whereOr(conditions: WhereCondition[]): this {
+ if (conditions.length === 0) {
+ return this;
+ }
+ const combinedClause = conditions.map((c) => `(${c.clause})`).join(" OR ");
+ this.whereClauses.push(`(${combinedClause})`);
+ for (const condition of conditions) {
+ if (condition.params) {
+ Object.assign(this.params, condition.params);
+ }
+ }
+ return this;
+ }
+
groupBy(clause: string): this {
this.groupByClause = clause;
return this;
@@ -153,6 +172,20 @@ export class ClickhouseQueryFastBuilder> {
return this;
}
+ whereOr(conditions: WhereCondition[]): this {
+ if (conditions.length === 0) {
+ return this;
+ }
+ const combinedClause = conditions.map((c) => `(${c.clause})`).join(" OR ");
+ this.whereClauses.push(`(${combinedClause})`);
+ for (const condition of conditions) {
+ if (condition.params) {
+ Object.assign(this.params, condition.params);
+ }
+ }
+ return this;
+ }
+
groupBy(clause: string): this {
this.groupByClause = clause;
return this;
diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts
index 47c2f34f2f5..de6bbb44e88 100644
--- a/internal-packages/clickhouse/src/index.ts
+++ b/internal-packages/clickhouse/src/index.ts
@@ -61,11 +61,6 @@ export type { OutputColumnMetadata } from "@internal/tsql";
// Errors
export { QueryError } from "./client/errors.js";
-export type LogsQuerySettings = {
- list?: ClickHouseSettings;
- detail?: ClickHouseSettings;
-};
-
export type ClickhouseCommonConfig = {
keepAlive?: {
enabled?: boolean;
@@ -80,7 +75,6 @@ export type ClickhouseCommonConfig = {
response?: boolean;
};
maxOpenConnections?: number;
- logsQuerySettings?: LogsQuerySettings;
};
export type ClickHouseConfig =
@@ -104,11 +98,9 @@ export class ClickHouse {
public readonly writer: ClickhouseWriter;
private readonly logger: Logger;
private _splitClients: boolean;
- private readonly logsQuerySettings?: LogsQuerySettings;
constructor(config: ClickHouseConfig) {
this.logger = config.logger ?? new Logger("ClickHouse", config.logLevel ?? "debug");
- this.logsQuerySettings = config.logsQuerySettings;
if (config.url) {
const url = new URL(config.url);
@@ -220,13 +212,13 @@ export class ClickHouse {
traceSummaryQueryBuilder: getTraceSummaryQueryBuilderV2(this.reader),
traceDetailedSummaryQueryBuilder: getTraceDetailedSummaryQueryBuilderV2(this.reader),
spanDetailsQueryBuilder: getSpanDetailsQueryBuilderV2(this.reader),
- logDetailQueryBuilder: getLogDetailQueryBuilderV2(this.reader, this.logsQuerySettings?.detail),
+ logDetailQueryBuilder: getLogDetailQueryBuilderV2(this.reader),
};
}
get taskEventsSearch() {
return {
- logsListQueryBuilder: getLogsSearchListQueryBuilder(this.reader, this.logsQuerySettings?.list),
+ logsListQueryBuilder: getLogsSearchListQueryBuilder(this.reader),
};
}
}
diff --git a/internal-packages/clickhouse/src/taskEvents.ts b/internal-packages/clickhouse/src/taskEvents.ts
index 73e2d8344ed..2109c498013 100644
--- a/internal-packages/clickhouse/src/taskEvents.ts
+++ b/internal-packages/clickhouse/src/taskEvents.ts
@@ -256,10 +256,7 @@ export const LogsSearchListResult = z.object({
export type LogsSearchListResult = z.output;
-export function getLogsSearchListQueryBuilder(
- ch: ClickhouseReader,
- settings?: ClickHouseSettings
-) {
+export function getLogsSearchListQueryBuilder(ch: ClickhouseReader) {
return ch.queryBuilderFast({
name: "getLogsSearchList",
table: "trigger_dev.task_events_search_v1",
@@ -280,7 +277,6 @@ export function getLogsSearchListQueryBuilder(
"attributes_text",
"triggered_timestamp",
],
- settings,
});
}
@@ -304,7 +300,7 @@ export const LogDetailV2Result = z.object({
export type LogDetailV2Result = z.output;
-export function getLogDetailQueryBuilderV2(ch: ClickhouseReader, settings?: ClickHouseSettings) {
+export function getLogDetailQueryBuilderV2(ch: ClickhouseReader) {
return ch.queryBuilderFast({
name: "getLogDetail",
table: "trigger_dev.task_events_v2",
@@ -324,6 +320,5 @@ export function getLogDetailQueryBuilderV2(ch: ClickhouseReader, settings?: Clic
"duration",
"attributes_text",
],
- settings,
});
}
\ No newline at end of file