improv(webapp): Add new table for optimized logs search (#3036)

##  Checklist

- [X] I have followed every step in the [contributing
guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
- [X] The PR title follows the convention.
- [X] I ran and tested the code works

---

## Testing

Tested the migration on test env and locally.
Tested query and merge performance.
Generated tasks and observed the ingested data and searches.

---

## Changelog

* New ClickHouse table & MV (task_events_search_v1): A search-optimized
materialized view that filters out debug events, partial spans, and
empty span events at ingestion time.
* ClickHouse client updates: New getLogsSearchListQueryBuilder and
taskEventsSearch accessor on the ClickHouse class.
* LogsListPresenter: Switches to the new search table, uses
triggered_timestamp for cursor pagination instead of unixTimestamp.
  *  Spans route: Also switches to the new search query builder.
* Seed spanSpammer task: Adds a 10s trace with events and metadata
operations for testing.
This commit is contained in:
Mihai Popescu
2026-02-13 16:28:14 +02:00
committed by GitHub
parent 4b7f67604a
commit a8f9a90280
11 changed files with 139 additions and 224 deletions
@@ -234,7 +234,7 @@ function DetailsTab({ log, runPath, searchTerm }: { log: LogEntry; runPath: stri
<div className="mb-6">
<Header3 className="mb-2">Timestamp</Header3>
<div className="text-sm text-text-dimmed">
<DateTimeAccurate date={log.startTime} />
<DateTimeAccurate date={log.triggeredTimestamp} />
</div>
</div>
@@ -162,7 +162,7 @@ export function LogsTable({
boxShadow: getLevelBoxShadow(log.level),
}}
>
<DateTimeAccurate date={log.startTime} />
<DateTimeAccurate date={log.triggeredTimestamp} />
</TableCell>
<TableCell className="min-w-24">
<TruncatedCopyableValue value={log.runId} />
@@ -81,12 +81,17 @@ export class LogDetailPresenter {
// Ignore parse errors
}
const durationMs = (typeof log.duration === "number" ? log.duration : Number(log.duration)) / 1_000_000;
return {
// Use :: separator to match LogsListPresenter format
id: `${log.trace_id}::${log.span_id}::${log.run_id}::${log.start_time}`,
runId: log.run_id,
taskIdentifier: log.task_identifier,
startTime: convertClickhouseDateTime64ToJsDate(log.start_time).toISOString(),
triggeredTimestamp: new Date(
convertClickhouseDateTime64ToJsDate(log.start_time).getTime() + durationMs
).toISOString(),
traceId: log.trace_id,
spanId: log.span_id,
parentSpanId: log.parent_span_id || null,
@@ -50,7 +50,6 @@ export type LogsListOptions = {
retentionLimitDays?: number;
// search
search?: string;
includeDebugLogs?: boolean;
// pagination
direction?: Direction;
cursor?: string;
@@ -69,7 +68,6 @@ export const LogsListOptionsSchema = z.object({
defaultPeriod: z.string().optional(),
retentionLimitDays: z.number().int().positive().optional(),
search: z.string().max(1000).optional(),
includeDebugLogs: z.boolean().optional(),
direction: z.enum(["forward", "backward"]).optional(),
cursor: z.string().optional(),
pageSize: z.number().int().positive().max(1000).optional(),
@@ -83,14 +81,16 @@ export type LogsListAppliedFilters = LogsList["filters"];
// Cursor is a base64 encoded JSON of the pagination keys
type LogCursor = {
organizationId: string;
environmentId: string;
unixTimestamp: number;
triggeredTimestamp: string; // DateTime64(9) string
traceId: string;
};
const LogCursorSchema = z.object({
organizationId: z.string(),
environmentId: z.string(),
unixTimestamp: z.number(),
triggeredTimestamp: z.string(),
traceId: z.string(),
});
@@ -116,33 +116,16 @@ function decodeCursor(cursor: string): LogCursor | null {
function levelToKindsAndStatuses(level: LogLevel): { kinds?: string[]; statuses?: string[] } {
switch (level) {
case "DEBUG":
return { kinds: ["DEBUG_EVENT", "LOG_DEBUG"] };
return { kinds: ["LOG_DEBUG"] };
case "INFO":
return { kinds: ["LOG_INFO", "LOG_LOG"] };
return { kinds: ["LOG_INFO", "LOG_LOG", "SPAN"] };
case "WARN":
return { kinds: ["LOG_WARN"] };
case "ERROR":
return { kinds: ["LOG_ERROR"], statuses: ["ERROR"] };
return { kinds: ["LOG_ERROR", "SPAN_EVENT"], statuses: ["ERROR"] };
}
}
function convertDateToNanoseconds(date: Date): bigint {
return BigInt(date.getTime()) * 1_000_000n;
}
function formatNanosecondsForClickhouse(ns: bigint): string {
const nsString = ns.toString();
// Handle negative numbers (dates before 1970-01-01)
if (nsString.startsWith("-")) {
const absString = nsString.slice(1);
const padded = absString.padStart(19, "0");
return "-" + padded.slice(0, 10) + "." + padded.slice(10);
}
// Pad positive numbers to 19 digits to ensure correct slicing
const padded = nsString.padStart(19, "0");
return padded.slice(0, 10) + "." + padded.slice(10);
}
export class LogsListPresenter extends BasePresenter {
constructor(
private readonly replica: PrismaClientOrTransaction,
@@ -166,7 +149,6 @@ export class LogsListPresenter extends BasePresenter {
to,
cursor,
pageSize = DEFAULT_PAGE_SIZE,
includeDebugLogs = true,
defaultPeriod,
retentionLimitDays,
}: LogsListOptions
@@ -252,7 +234,7 @@ export class LogsListPresenter extends BasePresenter {
);
}
const queryBuilder = this.clickhouse.taskEventsV2.logsListQueryBuilder();
const queryBuilder = this.clickhouse.taskEventsSearch.logsListQueryBuilder();
queryBuilder.where("environment_id = {environmentId: String}", {
environmentId,
@@ -265,27 +247,16 @@ export class LogsListPresenter extends BasePresenter {
if (effectiveFrom) {
const fromNs = convertDateToNanoseconds(effectiveFrom);
queryBuilder.where("inserted_at >= {insertedAtStart: DateTime64(3)}", {
insertedAtStart: convertDateToClickhouseDateTime(effectiveFrom),
queryBuilder.where("triggered_timestamp >= {triggeredAtStart: DateTime64(3)}", {
triggeredAtStart: convertDateToClickhouseDateTime(effectiveFrom),
});
queryBuilder.where("start_time >= {fromTime: String}", {
fromTime: formatNanosecondsForClickhouse(fromNs),
});
}
if (effectiveTo) {
const clampedTo = effectiveTo > new Date() ? new Date() : effectiveTo;
const toNs = convertDateToNanoseconds(clampedTo);
queryBuilder.where("inserted_at <= {insertedAtEnd: DateTime64(3)}", {
insertedAtEnd: convertDateToClickhouseDateTime(clampedTo),
});
queryBuilder.where("start_time <= {toTime: String}", {
toTime: formatNanosecondsForClickhouse(toNs),
queryBuilder.where("triggered_timestamp <= {triggeredAtEnd: DateTime64(3)}", {
triggeredAtEnd: convertDateToClickhouseDateTime(clampedTo),
});
}
@@ -349,39 +320,20 @@ export class LogsListPresenter extends BasePresenter {
}
}
// Debug logs are available only to admins
if (includeDebugLogs === false) {
queryBuilder.where("kind NOT IN {debugKinds: Array(String)}", {
debugKinds: ["DEBUG_EVENT"],
});
queryBuilder.where("NOT ((kind = 'LOG_INFO') AND (attributes_text = '{}'))");
}
queryBuilder.where("kind NOT IN {debugSpans: Array(String)}", {
debugSpans: ["SPAN", "ANCESTOR_OVERRIDE", "SPAN_EVENT"],
});
// kindCondition += ` `;
// params["excluded_statuses"] = ["SPAN", "ANCESTOR_OVERRIDE", "SPAN_EVENT"];
queryBuilder.where("NOT (kind = 'SPAN' AND status = 'PARTIAL')");
// Cursor pagination
// Cursor pagination using explicit lexicographic comparison
// Must mirror the ORDER BY columns: (organization_id, environment_id, triggered_timestamp, trace_id)
const decodedCursor = cursor ? decodeCursor(cursor) : null;
if (decodedCursor) {
queryBuilder.where(
"(environment_id, toUnixTimestamp(start_time), trace_id) < ({cursorEnvId: String}, {cursorUnixTimestamp: Int64}, {cursorTraceId: String})",
`(triggered_timestamp < {cursorTriggeredTimestamp: String} OR (triggered_timestamp = {cursorTriggeredTimestamp: String} AND trace_id < {cursorTraceId: String}))`,
{
cursorEnvId: decodedCursor.environmentId,
cursorUnixTimestamp: decodedCursor.unixTimestamp,
cursorTriggeredTimestamp: decodedCursor.triggeredTimestamp,
cursorTraceId: decodedCursor.traceId,
}
);
}
queryBuilder.orderBy("environment_id DESC, toUnixTimestamp(start_time) DESC, trace_id DESC");
queryBuilder.orderBy("triggered_timestamp DESC, trace_id DESC");
// Limit + 1 to check if there are more results
queryBuilder.limit(pageSize + 1);
@@ -399,10 +351,10 @@ export class LogsListPresenter extends BasePresenter {
let nextCursor: string | undefined;
if (hasMore && logs.length > 0) {
const lastLog = logs[logs.length - 1];
const unixTimestamp = Math.floor(new Date(lastLog.start_time).getTime() / 1000);
nextCursor = encodeCursor({
organizationId,
environmentId,
unixTimestamp,
triggeredTimestamp: lastLog.triggered_timestamp,
traceId: lastLog.trace_id,
});
}
@@ -430,6 +382,9 @@ export class LogsListPresenter extends BasePresenter {
runId: log.run_id,
taskIdentifier: log.task_identifier,
startTime: convertClickhouseDateTime64ToJsDate(log.start_time).toISOString(),
triggeredTimestamp: convertClickhouseDateTime64ToJsDate(
log.triggered_timestamp
).toISOString(),
traceId: log.trace_id,
spanId: log.span_id,
parentSpanId: log.parent_span_id || null,
@@ -36,7 +36,6 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "~/components/primitives/Resizable";
import { Switch } from "~/components/primitives/Switch";
import { Button } from "~/components/primitives/Buttons";
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags.server";
@@ -95,7 +94,6 @@ async function hasLogsPageAccess(
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const user = await requireUser(request);
const userId = user.id;
const isAdmin = user.admin || user.isImpersonating;
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
@@ -126,7 +124,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const runId = url.searchParams.get("runId") ?? undefined;
const search = url.searchParams.get("search") ?? undefined;
const levels = parseLevelsFromUrl(url);
const showDebug = url.searchParams.get("showDebug") === "true";
const period = url.searchParams.get("period") ?? undefined;
const fromStr = url.searchParams.get("from");
const toStr = url.searchParams.get("to");
@@ -150,7 +147,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
period,
from,
to,
includeDebugLogs: isAdmin && showDebug,
defaultPeriod: "1h",
retentionLimitDays
})
@@ -163,15 +159,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
return typeddefer({
data: listPromise,
isAdmin,
showDebug,
defaultPeriod: "1h",
retentionLimitDays,
});
};
export default function Page() {
const { data, isAdmin, showDebug, defaultPeriod, retentionLimitDays } =
const { data, defaultPeriod, retentionLimitDays } =
useTypedLoaderData<typeof loader>();
return (
@@ -199,8 +193,6 @@ export default function Page() {
errorElement={
<div className="grid h-full max-h-full grid-rows-[2.5rem_auto_1fr] overflow-hidden">
<FiltersBar
isAdmin={isAdmin}
showDebug={showDebug}
defaultPeriod={defaultPeriod}
retentionLimitDays={retentionLimitDays}
/>
@@ -218,8 +210,6 @@ export default function Page() {
return (
<div className="grid h-full max-h-full grid-rows-[2.5rem_auto_1fr] overflow-hidden">
<FiltersBar
isAdmin={isAdmin}
showDebug={showDebug}
defaultPeriod={defaultPeriod}
retentionLimitDays={retentionLimitDays}
/>
@@ -235,15 +225,11 @@ export default function Page() {
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden">
<FiltersBar
list={result}
isAdmin={isAdmin}
showDebug={showDebug}
defaultPeriod={defaultPeriod}
retentionLimitDays={retentionLimitDays}
/>
<LogsList
list={result}
isAdmin={isAdmin}
showDebug={showDebug}
defaultPeriod={defaultPeriod}
/>
</div>
@@ -258,14 +244,10 @@ export default function Page() {
function FiltersBar({
list,
isAdmin,
showDebug,
defaultPeriod,
retentionLimitDays,
}: {
list?: Exclude<Awaited<UseDataFunctionReturn<typeof loader>["data"]>, { error: string }>;
isAdmin: boolean;
showDebug: boolean;
defaultPeriod?: string;
retentionLimitDays: number;
}) {
@@ -280,16 +262,6 @@ function FiltersBar({
searchParams.has("from") ||
searchParams.has("to");
const handleDebugToggle = useCallback((checked: boolean) => {
const url = new URL(window.location.href);
if (checked) {
url.searchParams.set("showDebug", "true");
} else {
url.searchParams.delete("showDebug");
}
window.location.href = url.toString();
}, []);
return (
<div className="flex items-start justify-between gap-x-2 border-b border-grid-bright p-2">
<div className="flex flex-row flex-wrap items-center gap-1">
@@ -329,16 +301,6 @@ function FiltersBar({
</>
)}
</div>
<div className="flex items-center gap-2">
{isAdmin && (
<Switch
variant="small"
label="Debug"
checked={showDebug}
onCheckedChange={handleDebugToggle}
/>
)}
</div>
</div>
);
}
@@ -347,8 +309,6 @@ function LogsList({
list,
}: {
list: Exclude<Awaited<UseDataFunctionReturn<typeof loader>["data"]>, { error: string }>; //exclude error, it is handled
isAdmin: boolean;
showDebug: boolean;
defaultPeriod?: string;
}) {
const navigation = useNavigation();
@@ -1,100 +0,0 @@
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/node";
import { requireUserId } from "~/services/session.server";
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { clickhouseClient } from "~/services/clickhouseInstance.server";
// Convert ClickHouse kind to display level
function kindToLevel(
kind: string
): "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR" | "LOG" {
switch (kind) {
case "DEBUG_EVENT":
case "LOG_DEBUG":
return "DEBUG";
case "LOG_INFO":
return "INFO";
case "LOG_WARN":
return "WARN";
case "LOG_ERROR":
return "ERROR";
case "LOG_LOG":
return "LOG";
case "SPAN":
case "ANCESTOR_OVERRIDE":
case "SPAN_EVENT":
default:
return "TRACE";
}
}
// Fetch related spans for a log entry from the same trace
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 });
}
// Get trace ID and run ID from query params
const url = new URL(request.url);
const traceId = url.searchParams.get("traceId");
const runId = url.searchParams.get("runId");
const currentSpanId = url.searchParams.get("spanId");
if (!traceId || !runId) {
throw new Response("Trace ID and Run ID are required", { status: 400 });
}
// Query ClickHouse for related spans in the same trace
const queryBuilder = clickhouseClient.taskEventsV2.logsListQueryBuilder();
queryBuilder.where("environment_id = {environmentId: String}", {
environmentId: environment.id,
});
queryBuilder.where("trace_id = {traceId: String}", { traceId });
queryBuilder.where("run_id = {runId: String}", { runId });
// Order by start time to show spans in chronological order
queryBuilder.orderBy("start_time ASC");
queryBuilder.limit(50);
const [queryError, records] = await queryBuilder.execute();
if (queryError) {
throw queryError;
}
const results = records || [];
const spans = results.map((row) => ({
id: `${row.trace_id}::${row.span_id}::${row.run_id}::${row.start_time}`,
spanId: row.span_id,
parentSpanId: row.parent_span_id || null,
message: row.message.substring(0, 200), // Truncate for list view
kind: row.kind,
level: kindToLevel(row.kind),
status: row.status,
startTime: new Date(Number(row.start_time) / 1_000_000).toISOString(),
duration: Number(row.duration),
isCurrent: row.span_id === currentSpanId,
}));
return json({ spans });
};
@@ -21,7 +21,6 @@ function parseLevelsFromUrl(url: URL): LogLevel[] | undefined {
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const user = await requireUser(request);
const userId = user.id;
const isAdmin = user?.admin || user?.isImpersonating;
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
@@ -46,7 +45,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const search = url.searchParams.get("search") ?? undefined;
const cursor = url.searchParams.get("cursor") ?? undefined;
const levels = parseLevelsFromUrl(url);
const showDebug = url.searchParams.get("showDebug") === "true";
const period = url.searchParams.get("period") ?? undefined;
const fromStr = url.searchParams.get("from");
const toStr = url.searchParams.get("to");
@@ -67,7 +65,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
from,
to,
levels,
includeDebugLogs: isAdmin && showDebug,
defaultPeriod: "1h",
retentionLimitDays,
}) as any; // Validated by LogsListOptionsSchema at runtime
@@ -0,0 +1,63 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS trigger_dev.task_events_search_v1
(
environment_id String,
organization_id String,
project_id String,
triggered_timestamp DateTime64(9) CODEC(Delta(8), ZSTD(1)),
trace_id String CODEC(ZSTD(1)),
span_id String CODEC(ZSTD(1)),
run_id String CODEC(ZSTD(1)),
task_identifier String CODEC(ZSTD(1)),
start_time DateTime64(9) CODEC(Delta(8), ZSTD(1)),
inserted_at DateTime64(3),
message String CODEC(ZSTD(1)),
kind LowCardinality(String) CODEC(ZSTD(1)),
status LowCardinality(String) CODEC(ZSTD(1)),
duration UInt64 CODEC(ZSTD(1)),
parent_span_id String CODEC(ZSTD(1)),
attributes_text String CODEC(ZSTD(1)),
INDEX idx_run_id run_id TYPE bloom_filter(0.001) GRANULARITY 1,
INDEX idx_message_text_search lower(message) TYPE ngrambf_v1(3, 32768, 2, 0) GRANULARITY 1,
INDEX idx_attributes_text_search lower(attributes_text) TYPE ngrambf_v1(3, 32768, 2, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(triggered_timestamp)
ORDER BY (organization_id, environment_id, triggered_timestamp, trace_id)
--Right now we have maximum retention of up to 30 days based on plan.
--We put a logical limit for now, the 90 DAY TTL is just a backup
--This might need to be updated for longer retention periods
TTL toDateTime(triggered_timestamp) + INTERVAL 90 DAY
SETTINGS ttl_only_drop_parts = 1;
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';
-- +goose Down
DROP VIEW IF EXISTS trigger_dev.task_events_search_mv_v1;
DROP TABLE IF EXISTS trigger_dev.task_events_search_v1;
+7 -2
View File
@@ -23,8 +23,8 @@ import {
getTraceSummaryQueryBuilderV2,
insertTaskEvents,
insertTaskEventsV2,
getLogsListQueryBuilderV2,
getLogDetailQueryBuilderV2,
getLogsSearchListQueryBuilder,
} from "./taskEvents.js";
import { Logger, type LogLevel } from "@trigger.dev/core/logger";
import type { Agent as HttpAgent } from "http";
@@ -220,8 +220,13 @@ export class ClickHouse {
traceSummaryQueryBuilder: getTraceSummaryQueryBuilderV2(this.reader),
traceDetailedSummaryQueryBuilder: getTraceDetailedSummaryQueryBuilderV2(this.reader),
spanDetailsQueryBuilder: getSpanDetailsQueryBuilderV2(this.reader),
logsListQueryBuilder: getLogsListQueryBuilderV2(this.reader, this.logsQuerySettings?.list),
logDetailQueryBuilder: getLogDetailQueryBuilderV2(this.reader, this.logsQuerySettings?.detail),
};
}
get taskEventsSearch() {
return {
logsListQueryBuilder: getLogsSearchListQueryBuilder(this.reader, this.logsQuerySettings?.list),
};
}
}
+14 -8
View File
@@ -231,11 +231,12 @@ export function getSpanDetailsQueryBuilderV2(
});
}
// ============================================================================
// Logs List Query Builders (for aggregated logs page)
// Search Table Query Builders (for logs page, using task_events_search_v1)
// ============================================================================
export const LogsListResult = z.object({
export const LogsSearchListResult = z.object({
environment_id: z.string(),
organization_id: z.string(),
project_id: z.string(),
@@ -250,14 +251,18 @@ export const LogsListResult = z.object({
status: z.string(),
duration: z.number().or(z.string()),
attributes_text: z.string(),
triggered_timestamp: z.string(),
});
export type LogsListResult = z.output<typeof LogsListResult>;
export type LogsSearchListResult = z.output<typeof LogsSearchListResult>;
export function getLogsListQueryBuilderV2(ch: ClickhouseReader, settings?: ClickHouseSettings) {
return ch.queryBuilderFast<LogsListResult>({
name: "getLogsList",
table: "trigger_dev.task_events_v2",
export function getLogsSearchListQueryBuilder(
ch: ClickhouseReader,
settings?: ClickHouseSettings
) {
return ch.queryBuilderFast<LogsSearchListResult>({
name: "getLogsSearchList",
table: "trigger_dev.task_events_search_v1",
columns: [
"environment_id",
"organization_id",
@@ -272,7 +277,8 @@ export function getLogsListQueryBuilderV2(ch: ClickhouseReader, settings?: Click
"kind",
"status",
"duration",
"attributes_text"
"attributes_text",
"triggered_timestamp",
],
settings,
});
+25 -1
View File
@@ -1,4 +1,4 @@
import { logger, task, wait } from "@trigger.dev/sdk/v3";
import { logger, task, wait, metadata } from "@trigger.dev/sdk/v3";
const CONFIG = {
delayBetweenBatchesSeconds: 0.2,
@@ -14,6 +14,22 @@ export const SpanSpammerTask = task({
const context = { payload, ctx };
let logCount = 0;
// 30s trace with events every 5s
await logger.trace("10s-span", async () => {
const totalSeconds = 10;
const intervalSeconds = 2;
const totalEvents = totalSeconds / intervalSeconds;
logger.info("Starting 30s span", context);
for (let i = 1; i <= totalEvents; i++) {
await wait.for({ seconds: intervalSeconds });
logger.info(`Inner event ${i}/${totalEvents} at ${i * intervalSeconds}s`, context);
}
logger.info("Completed 30s span", context);
});
logger.info("Starting span spammer task", context);
logger.warn("This will generate a lot of logs", context);
@@ -36,6 +52,14 @@ export const SpanSpammerTask = task({
emitBatch("This is a test log!!! Log number: ");
}
metadata.parent.set("childStatus", "running");
metadata.parent.increment("completedChildren", 1);
// Update the root run's metadata (top-level run in the chain)
metadata.root.set("deepChildStatus", "done");
metadata.root.append("completedTasks", "child-task");
logger.info("Completed span spammer task", context);
return { message: `Created ${logCount} logs` };
},