Skip to content

Commit 41f857e

Browse files
committed
fix(webapp): fail open on queue metrics and honor sparkline total override
The queues list tolerates a metrics query failure by rendering without metrics and logging a warning. UsageSparkline renders its total override even when every bucket is zero. The queue detail page returns 404 and its loader skips the metrics query when the feature flag is off. The seed script validates bucket size and only writes ClickHouse against a local host.
1 parent 4d3ca7b commit 41f857e

5 files changed

Lines changed: 42 additions & 21 deletions

File tree

apps/webapp/app/components/primitives/UsageSparkline.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export function UsageSparkline({
5050
formatTotal,
5151
totalClassName = "text-blue-400",
5252
}: UsageSparklineProps) {
53-
if (!data || data.every((v) => v === 0)) {
53+
const hasTotalOverride = totalOverride !== undefined;
54+
if (!data || data.length === 0 || (data.every((v) => v === 0) && !hasTotalOverride)) {
5455
return <span className="text-text-dimmed"></span>;
5556
}
5657

apps/webapp/app/env.server.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -899,15 +899,14 @@ const EnvironmentSchema = z
899899
QUEUE_METRICS_EMIT_ENABLED: z.string().default("0"),
900900
QUEUE_METRICS_CONSUMER_ENABLED: z.string().default("0"),
901901
QUEUE_METRICS_STREAM_SHARD_COUNT: z.coerce.number().int().default(4),
902-
QUEUE_METRICS_STREAM_MAXLEN: z.coerce.number().int().default(2_000_000),
903902
QUEUE_METRICS_CONSUMER_BATCH_SIZE: z.coerce.number().int().default(1000),
904903
// Counter stream (exact counts, loss-intolerant). Unset host => the run-queue Redis;
905904
// set it to a dedicated instance so counter backlog never competes with the run queue.
906905
QUEUE_METRICS_REDIS_HOST: z.string().optional(),
907906
QUEUE_METRICS_REDIS_PORT: z.coerce.number().optional(),
908907
QUEUE_METRICS_REDIS_USERNAME: z.string().optional(),
909908
QUEUE_METRICS_REDIS_PASSWORD: z.string().optional(),
910-
QUEUE_METRICS_REDIS_TLS_DISABLED: z.string().default("false"),
909+
QUEUE_METRICS_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"),
911910
QUEUE_METRICS_COUNTER_STREAM_MAXLEN: z.coerce.number().int().default(8_000_000),
912911
// TTL (seconds) on the per-(queue,op) cumulative odometer key, refreshed on every write.
913912
// Idle-past-TTL queues purge and self-heal (restart from 1) on return; default 7 days.

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
} from "~/presenters/v3/QueueMetricsPresenter.server";
7474
import { UsageSparkline } from "~/components/primitives/UsageSparkline";
7575
import { Area, AreaChart, ResponsiveContainer } from "recharts";
76+
import { logger } from "~/services/logger.server";
7677
import { requireUserId } from "~/services/session.server";
7778
import { cn } from "~/utils/cn";
7879
import { ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT } from "~/utils/environmentPauseSource";
@@ -160,19 +161,27 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
160161
} | null = null;
161162

162163
if (queueMetricsUiEnabled && queues.success) {
163-
const presenter = new QueueMetricsPresenter();
164-
const queueNames = queues.queues.map((q) => (q.type === "task" ? `task/${q.name}` : q.name));
165-
const queueMetrics =
166-
queueNames.length > 0
167-
? await presenter.getQueueListMetrics({ environment, queueNames, window: period })
168-
: null;
169-
if (queueMetrics) {
170-
metrics = {
171-
window: queueMetrics.window,
172-
bucketStartMs: queueMetrics.bucketStartMs,
173-
bucketIntervalMs: queueMetrics.bucketIntervalMs,
174-
byQueue: Object.fromEntries(queueMetrics.byQueue),
175-
};
164+
// Metrics are additive observability; a ClickHouse hiccup must not take down queue
165+
// management. Fail open to metrics: null instead of bubbling to the page-level 400.
166+
try {
167+
const presenter = new QueueMetricsPresenter();
168+
const queueNames = queues.queues.map((q) =>
169+
q.type === "task" ? `task/${q.name}` : q.name
170+
);
171+
const queueMetrics =
172+
queueNames.length > 0
173+
? await presenter.getQueueListMetrics({ environment, queueNames, window: period })
174+
: null;
175+
if (queueMetrics) {
176+
metrics = {
177+
window: queueMetrics.window,
178+
bucketStartMs: queueMetrics.bucketStartMs,
179+
bucketIntervalMs: queueMetrics.bucketIntervalMs,
180+
byQueue: Object.fromEntries(queueMetrics.byQueue),
181+
};
182+
}
183+
} catch (error) {
184+
logger.warn("Queue list metrics unavailable, rendering without them", { error });
176185
}
177186
}
178187

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ function QueueStats({
443443
loading={showLoading}
444444
/>
445445
<Stat
446-
label="Worst delay p95"
446+
label="Delay p95"
447447
value={worstP95 > 0 ? formatWaitMs(worstP95) : "–"}
448448
loading={showLoading}
449449
className={worstP95 >= 60_000 ? "text-warning" : undefined}

apps/webapp/seed-queue-metrics.mts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ function simulateBucket(
282282
env_running: envRunning,
283283
env_queued: envQueued,
284284
env_limit: envLimit,
285-
throttled: running[q] >= limit[q] && queued[q] > 0 ? 1 : 0,
285+
throttled: queued[q] > 0 && (running[q] >= limit[q] || scale < 1) ? 1 : 0,
286286
};
287287
rows.push(gauge);
288288

@@ -317,11 +317,13 @@ function clickhouse(): ClickHouse {
317317
console.error("CLICKHOUSE_URL not set");
318318
process.exit(1);
319319
}
320-
if (/\.clickhouse\.cloud|prod/i.test(clickhouseUrl)) {
321-
console.error(`Refusing to run against a non-local ClickHouse: ${clickhouseUrl}`);
320+
const url = new URL(clickhouseUrl);
321+
// Allowlist local hosts only (this script TRUNCATEs), and never echo the URL (it carries creds).
322+
const localHosts = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
323+
if (!localHosts.has(url.hostname)) {
324+
console.error(`Refusing to run against a non-local ClickHouse host: ${url.hostname}`);
322325
process.exit(1);
323326
}
324-
const url = new URL(clickhouseUrl);
325327
url.searchParams.delete("secure");
326328
return new ClickHouse({ url: url.toString(), name: "queue-metrics-simulator" });
327329
}
@@ -419,8 +421,18 @@ async function main() {
419421
process.exit(1);
420422
}
421423
const bucketSec = Number(flags.bucket ?? 10);
424+
if (!Number.isFinite(bucketSec) || bucketSec <= 0) {
425+
console.error(`--bucket must be a positive number of seconds, got: ${flags.bucket}`);
426+
process.exit(1);
427+
}
422428
const windowSec = parseDuration(flags.window ?? "2h");
423429
const totalBuckets = Math.floor(windowSec / bucketSec);
430+
if (!Number.isFinite(totalBuckets) || totalBuckets <= 0) {
431+
console.error(
432+
`--window must be longer than --bucket (got ${windowSec}s window, ${bucketSec}s bucket)`
433+
);
434+
process.exit(1);
435+
}
424436
const seed = Number(flags.seed ?? 1);
425437
const live = flags.live === "true";
426438

0 commit comments

Comments
 (0)