Skip to content

Commit 7691f82

Browse files
authored
Merge branch 'main' into fix(webapp)-logs-button
2 parents 4cfc055 + 23c327e commit 7691f82

File tree

62 files changed

+2678
-106
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2678
-106
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/sdk": patch
4+
---
5+
6+
Add OTEL metrics pipeline for task workers. Workers collect process CPU/memory, Node.js runtime metrics (event loop utilization, event loop delay, heap usage), and user-defined custom metrics via `otel.metrics.getMeter()`. Metrics are exported to ClickHouse with 10-second aggregation buckets and 1m/5m rollups, and are queryable through the dashboard query engine with typed attribute columns, `prettyFormat()` for human-readable values, and AI query support.

apps/webapp/app/components/code/QueryResultsChart.tsx

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import type { OutputColumnMetadata } from "@internal/clickhouse";
1+
import type { ColumnFormatType, OutputColumnMetadata } from "@internal/clickhouse";
2+
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
23
import { BarChart3, LineChart } from "lucide-react";
34
import { memo, useMemo } from "react";
5+
import { createValueFormatter } from "~/utils/columnFormat";
6+
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
47
import type { ChartConfig } from "~/components/primitives/charts/Chart";
58
import { Chart } from "~/components/primitives/charts/ChartCompound";
69
import { ChartBlankState } from "../primitives/charts/ChartBlankState";
@@ -855,8 +858,24 @@ export const QueryResultsChart = memo(function QueryResultsChart({
855858
};
856859
}, [isDateBased, timeGranularity]);
857860

858-
// Create dynamic Y-axis formatter based on data range
859-
const yAxisFormatter = useMemo(() => createYAxisFormatter(data, series), [data, series]);
861+
// Resolve the Y-axis column format for formatting
862+
const yAxisFormat = useMemo(() => {
863+
if (yAxisColumns.length === 0) return undefined;
864+
const col = columns.find((c) => c.name === yAxisColumns[0]);
865+
return (col?.format ?? col?.customRenderType) as ColumnFormatType | undefined;
866+
}, [yAxisColumns, columns]);
867+
868+
// Create dynamic Y-axis formatter based on data range and format
869+
const yAxisFormatter = useMemo(
870+
() => createYAxisFormatter(data, series, yAxisFormat),
871+
[data, series, yAxisFormat]
872+
);
873+
874+
// Create value formatter for tooltips and legend based on column format
875+
const tooltipValueFormatter = useMemo(
876+
() => createValueFormatter(yAxisFormat),
877+
[yAxisFormat]
878+
);
860879

861880
// Check if the group-by column has a runStatus customRenderType
862881
const groupByIsRunStatus = useMemo(() => {
@@ -1081,6 +1100,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10811100
showLegend={showLegend}
10821101
maxLegendItems={fullLegend ? Infinity : 5}
10831102
legendAggregation={config.aggregation}
1103+
legendValueFormatter={tooltipValueFormatter}
10841104
minHeight="300px"
10851105
fillContainer
10861106
onViewAllLegendItems={onViewAllLegendItems}
@@ -1093,6 +1113,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10931113
yAxisProps={yAxisProps}
10941114
stackId={stacked ? "stack" : undefined}
10951115
tooltipLabelFormatter={tooltipLabelFormatter}
1116+
tooltipValueFormatter={tooltipValueFormatter}
10961117
/>
10971118
</Chart.Root>
10981119
);
@@ -1110,6 +1131,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
11101131
showLegend={showLegend}
11111132
maxLegendItems={fullLegend ? Infinity : 5}
11121133
legendAggregation={config.aggregation}
1134+
legendValueFormatter={tooltipValueFormatter}
11131135
minHeight="300px"
11141136
fillContainer
11151137
onViewAllLegendItems={onViewAllLegendItems}
@@ -1122,16 +1144,21 @@ export const QueryResultsChart = memo(function QueryResultsChart({
11221144
yAxisProps={yAxisProps}
11231145
stacked={stacked && visibleSeries.length > 1}
11241146
tooltipLabelFormatter={tooltipLabelFormatter}
1147+
tooltipValueFormatter={tooltipValueFormatter}
11251148
lineType="linear"
11261149
/>
11271150
</Chart.Root>
11281151
);
11291152
});
11301153

11311154
/**
1132-
* Creates a Y-axis value formatter based on the data range
1155+
* Creates a Y-axis value formatter based on the data range and optional format hint
11331156
*/
1134-
function createYAxisFormatter(data: Record<string, unknown>[], series: string[]) {
1157+
function createYAxisFormatter(
1158+
data: Record<string, unknown>[],
1159+
series: string[],
1160+
format?: ColumnFormatType
1161+
) {
11351162
// Find min and max values across all series
11361163
let minVal = Infinity;
11371164
let maxVal = -Infinity;
@@ -1148,6 +1175,46 @@ function createYAxisFormatter(data: Record<string, unknown>[], series: string[])
11481175

11491176
const range = maxVal - minVal;
11501177

1178+
// Format-aware formatters
1179+
if (format === "bytes" || format === "decimalBytes") {
1180+
const divisor = format === "bytes" ? 1024 : 1000;
1181+
const units =
1182+
format === "bytes"
1183+
? ["B", "KiB", "MiB", "GiB", "TiB"]
1184+
: ["B", "KB", "MB", "GB", "TB"];
1185+
return (value: number): string => {
1186+
if (value === 0) return "0 B";
1187+
// Use consistent unit for all ticks based on max value
1188+
const i = Math.min(
1189+
Math.max(0, Math.floor(Math.log(Math.abs(maxVal || 1)) / Math.log(divisor))),
1190+
units.length - 1
1191+
);
1192+
const scaled = value / Math.pow(divisor, i);
1193+
return `${scaled.toFixed(scaled < 10 ? 1 : 0)} ${units[i]}`;
1194+
};
1195+
}
1196+
1197+
if (format === "percent") {
1198+
return (value: number): string => `${value.toFixed(range < 1 ? 2 : 1)}%`;
1199+
}
1200+
1201+
if (format === "duration") {
1202+
return (value: number): string => formatDurationMilliseconds(value, { style: "short" });
1203+
}
1204+
1205+
if (format === "durationSeconds") {
1206+
return (value: number): string =>
1207+
formatDurationMilliseconds(value * 1000, { style: "short" });
1208+
}
1209+
1210+
if (format === "costInDollars" || format === "cost") {
1211+
return (value: number): string => {
1212+
const dollars = format === "cost" ? value / 100 : value;
1213+
return formatCurrencyAccurate(dollars);
1214+
};
1215+
}
1216+
1217+
// Default formatter
11511218
return (value: number): string => {
11521219
// Use abbreviations for large numbers
11531220
if (Math.abs(value) >= 1_000_000) {

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { useCopy } from "~/hooks/useCopy";
3535
import { useOrganization } from "~/hooks/useOrganizations";
3636
import { useProject } from "~/hooks/useProject";
3737
import { cn } from "~/utils/cn";
38+
import { formatBytes, formatDecimalBytes, formatQuantity } from "~/utils/columnFormat";
3839
import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
3940
import { v3ProjectPath, v3RunPathFromFriendlyId } from "~/utils/pathBuilder";
4041
import { ChartBlankState } from "../primitives/charts/ChartBlankState";
@@ -66,9 +67,10 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
6667
if (value === null) return "NULL";
6768
if (value === undefined) return "";
6869

69-
// Handle custom render types
70-
if (column.customRenderType) {
71-
switch (column.customRenderType) {
70+
// Handle format hints (from prettyFormat() or auto-populated from customRenderType)
71+
const formatType = column.format ?? column.customRenderType;
72+
if (formatType) {
73+
switch (formatType) {
7274
case "duration":
7375
if (typeof value === "number") {
7476
return formatDurationMilliseconds(value, { style: "short" });
@@ -95,6 +97,26 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
9597
return value;
9698
}
9799
break;
100+
case "bytes":
101+
if (typeof value === "number") {
102+
return formatBytes(value);
103+
}
104+
break;
105+
case "decimalBytes":
106+
if (typeof value === "number") {
107+
return formatDecimalBytes(value);
108+
}
109+
break;
110+
case "percent":
111+
if (typeof value === "number") {
112+
return `${value.toFixed(2)}%`;
113+
}
114+
break;
115+
case "quantity":
116+
if (typeof value === "number") {
117+
return formatQuantity(value);
118+
}
119+
break;
98120
}
99121
}
100122

@@ -222,6 +244,21 @@ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number
222244
if (value === null) return 4; // "NULL"
223245
if (value === undefined) return 9; // "UNDEFINED"
224246

247+
// Handle format hint types - estimate their rendered width
248+
const fmt = column.format;
249+
if (fmt === "bytes" || fmt === "decimalBytes") {
250+
// e.g., "1.50 GiB" or "256.00 MB"
251+
return 12;
252+
}
253+
if (fmt === "percent") {
254+
// e.g., "45.23%"
255+
return 8;
256+
}
257+
if (fmt === "quantity") {
258+
// e.g., "1.50M"
259+
return 8;
260+
}
261+
225262
// Handle custom render types - estimate their rendered width
226263
if (column.customRenderType) {
227264
switch (column.customRenderType) {
@@ -263,6 +300,8 @@ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number
263300
return typeof value === "string" ? Math.min(value.length, 20) : 12;
264301
case "queue":
265302
return typeof value === "string" ? Math.min(value.length, 25) : 15;
303+
case "deploymentId":
304+
return typeof value === "string" ? Math.min(value.length, 25) : 20;
266305
}
267306
}
268307

@@ -394,6 +433,10 @@ function isRightAlignedColumn(column: OutputColumnMetadata): boolean {
394433
) {
395434
return true;
396435
}
436+
const fmt = column.format;
437+
if (fmt === "bytes" || fmt === "decimalBytes" || fmt === "percent" || fmt === "quantity") {
438+
return true;
439+
}
397440
return isNumericType(column.type);
398441
}
399442

@@ -476,6 +519,32 @@ function CellValue({
476519
return <pre className="text-text-dimmed">UNDEFINED</pre>;
477520
}
478521

522+
// Check format hint for new format types (from prettyFormat())
523+
if (column.format && !column.customRenderType) {
524+
switch (column.format) {
525+
case "bytes":
526+
if (typeof value === "number") {
527+
return <span className="tabular-nums">{formatBytes(value)}</span>;
528+
}
529+
break;
530+
case "decimalBytes":
531+
if (typeof value === "number") {
532+
return <span className="tabular-nums">{formatDecimalBytes(value)}</span>;
533+
}
534+
break;
535+
case "percent":
536+
if (typeof value === "number") {
537+
return <span className="tabular-nums">{value.toFixed(2)}%</span>;
538+
}
539+
break;
540+
case "quantity":
541+
if (typeof value === "number") {
542+
return <span className="tabular-nums">{formatQuantity(value)}</span>;
543+
}
544+
break;
545+
}
546+
}
547+
479548
// First check customRenderType for special rendering
480549
if (column.customRenderType) {
481550
switch (column.customRenderType) {
@@ -577,6 +646,19 @@ function CellValue({
577646
}
578647
return <span>{String(value)}</span>;
579648
}
649+
case "deploymentId": {
650+
if (typeof value === "string" && value.startsWith("deployment_")) {
651+
return (
652+
<SimpleTooltip
653+
content="Jump to deployment"
654+
disableHoverableContent
655+
hidden={!hovered}
656+
button={<TextLink to={`/deployments/${value}`}>{value}</TextLink>}
657+
/>
658+
);
659+
}
660+
return <span>{String(value)}</span>;
661+
}
580662
}
581663
}
582664

apps/webapp/app/components/primitives/charts/BigNumberCard.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { OutputColumnMetadata } from "@internal/tsql";
1+
import type { ColumnFormatType, OutputColumnMetadata } from "@internal/tsql";
22
import { Hash } from "lucide-react";
33
import { useMemo } from "react";
44
import type {
55
BigNumberAggregationType,
66
BigNumberConfiguration,
77
} from "~/components/metrics/QueryWidget";
8+
import { createValueFormatter } from "~/utils/columnFormat";
89
import { AnimatedNumber } from "../AnimatedNumber";
910
import { ChartBlankState } from "./ChartBlankState";
1011
import { Spinner } from "../Spinner";
@@ -130,6 +131,15 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
130131
return aggregateValues(values, aggregation);
131132
}, [rows, column, aggregation, sortDirection]);
132133

134+
// Look up column format for format-aware display
135+
const columnValueFormatter = useMemo(() => {
136+
const columnMeta = columns.find((c) => c.name === column);
137+
const formatType = (columnMeta?.format ?? columnMeta?.customRenderType) as
138+
| ColumnFormatType
139+
| undefined;
140+
return createValueFormatter(formatType);
141+
}, [columns, column]);
142+
133143
if (isLoading) {
134144
return (
135145
<div className="grid h-full place-items-center [container-type:size]">
@@ -142,14 +152,29 @@ export function BigNumberCard({ rows, columns, config, isLoading = false }: BigN
142152
return <ChartBlankState icon={Hash} message="No data to display" />;
143153
}
144154

155+
// Use format-aware formatter when available
156+
if (columnValueFormatter) {
157+
return (
158+
<div className="h-full w-full [container-type:size]">
159+
<div className="grid h-full w-full place-items-center">
160+
<div className="flex items-baseline gap-[0.15em] whitespace-nowrap text-[clamp(24px,12cqw,96px)] font-normal tabular-nums leading-none text-text-bright">
161+
{prefix && <span>{prefix}</span>}
162+
<span>{columnValueFormatter(result)}</span>
163+
{suffix && <span className="text-[0.4em] text-text-dimmed">{suffix}</span>}
164+
</div>
165+
</div>
166+
</div>
167+
);
168+
}
169+
145170
const { displayValue, unitSuffix, decimalPlaces } = abbreviate
146171
? abbreviateValue(result)
147172
: { displayValue: result, unitSuffix: undefined, decimalPlaces: getDecimalPlaces(result) };
148173

149174
return (
150175
<div className="h-full w-full [container-type:size]">
151176
<div className="grid h-full w-full place-items-center">
152-
<div className="flex items-baseline gap-[0.15em] whitespace-nowrap font-normal tabular-nums leading-none text-text-bright text-[clamp(24px,12cqw,96px)]">
177+
<div className="flex items-baseline gap-[0.15em] whitespace-nowrap text-[clamp(24px,12cqw,96px)] font-normal tabular-nums leading-none text-text-bright">
153178
{prefix && <span>{prefix}</span>}
154179
<AnimatedNumber value={displayValue} decimalPlaces={decimalPlaces} />
155180
{(unitSuffix || suffix) && (

apps/webapp/app/components/primitives/charts/Chart.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ const ChartTooltipContent = React.forwardRef<
104104
indicator?: "line" | "dot" | "dashed";
105105
nameKey?: string;
106106
labelKey?: string;
107+
/** Optional formatter for numeric values (e.g. bytes, duration) */
108+
valueFormatter?: (value: number) => string;
107109
}
108110
>(
109111
(
@@ -121,6 +123,7 @@ const ChartTooltipContent = React.forwardRef<
121123
color,
122124
nameKey,
123125
labelKey,
126+
valueFormatter,
124127
},
125128
ref
126129
) => {
@@ -221,9 +224,11 @@ const ChartTooltipContent = React.forwardRef<
221224
{itemConfig?.label || item.name}
222225
</span>
223226
</div>
224-
{item.value && (
227+
{item.value != null && (
225228
<span className="text-foreground font-mono font-medium tabular-nums">
226-
{item.value.toLocaleString()}
229+
{valueFormatter && typeof item.value === "number"
230+
? valueFormatter(item.value)
231+
: item.value.toLocaleString()}
227232
</span>
228233
)}
229234
</div>

0 commit comments

Comments
 (0)