Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
import { cn } from "~/lib/utils";
import { ArgoCDVerificationDisplay } from "./argocd/ArgoCD";
import { isArgoCDMeasurement } from "./argocd/argocd-metric";
import { PrometheusVerificationDisplay } from "./prometheus/Prometheus";
import { PrometheusIcon } from "./prometheus/PrometheusIcon";
import { isPrometheusProvider } from "./prometheus/prometheus-metric";
import { VerificationMetricStatus } from "./VerificationMetricStatus";

type JobVerification = WorkspaceEngine["schemas"]["JobVerification"];
Expand Down Expand Up @@ -152,6 +155,7 @@ function MetricDisplay({ metric }: { metric: VerificationMetricStatus }) {

const isArgoCD =
latestMeasurement != null && isArgoCDMeasurement(latestMeasurement.data);
const isPrometheus = isPrometheusProvider(metric.provider);

return (
<Collapsible open={open} onOpenChange={setOpen}>
Expand All @@ -163,6 +167,9 @@ function MetricDisplay({ metric }: { metric: VerificationMetricStatus }) {
open ? "rotate-90" : "",
)}
/>
{isPrometheus && (
<PrometheusIcon className="h-4 w-4 text-orange-500" />
)}
<span className="text-sm font-medium">{metric.name}</span>
<div className="grow" />
<VerificationMetricStatus metric={metric} />
Expand All @@ -171,7 +178,8 @@ function MetricDisplay({ metric }: { metric: VerificationMetricStatus }) {

<CollapsibleContent className="space-y-2 pl-6 text-xs">
{isArgoCD && <ArgoCDVerificationDisplay metric={metric} />}
{!isArgoCD && (
{isPrometheus && <PrometheusVerificationDisplay metric={metric} />}
{!isArgoCD && !isPrometheus && (
<>
<MetricSummaryDisplay metric={metric} />
{sortedMeasurements.map((measurement, idx) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { WorkspaceEngine } from "@ctrlplane/workspace-engine-sdk";
import { formatDistanceToNowStrict } from "date-fns";

import { cn } from "~/lib/utils";
import {
parsePrometheusMeasurement,
parsePrometheusProvider,
} from "./prometheus-metric";

type VerificationMetricStatus =
WorkspaceEngine["schemas"]["VerificationMetricStatus"];
type MetricMeasurement = VerificationMetricStatus["measurements"][number];

export function PrometheusVerificationDisplay({
metric,
}: {
metric: VerificationMetricStatus;
}) {
const provider = parsePrometheusProvider(metric.provider);
const sortedMeasurements = [...metric.measurements].sort(
(a, b) =>
new Date(b.measuredAt).getTime() - new Date(a.measuredAt).getTime(),
);

return (
<div className="space-y-3 pl-1 pr-2">
<ProviderInfo
query={provider?.query}
address={provider?.address}
successCondition={metric.successCondition}
/>
{sortedMeasurements.length > 0 && (
<MeasurementTrend measurements={sortedMeasurements} />
)}
</div>
);
}

function ProviderInfo({
query,
address,
successCondition,
}: {
query?: string;
address?: string;
successCondition: string;
}) {
return (
<div className="flex flex-col gap-1 text-xs">
{query != null && (
<div className="flex justify-between gap-4">
<span className="shrink-0 text-muted-foreground">Query</span>
<code className="truncate rounded bg-muted px-1.5 py-0.5 font-mono">
{query}
</code>
</div>
)}
<div className="flex justify-between gap-4">
<span className="shrink-0 text-muted-foreground">Condition</span>
<code className="truncate rounded bg-muted px-1.5 py-0.5 font-mono">
{successCondition}
</code>
</div>
{address != null && (
<div className="flex justify-between gap-4">
<span className="shrink-0 text-muted-foreground">Server</span>
<span className="truncate text-muted-foreground">{address}</span>
</div>
)}
</div>
);
}

function MeasurementTrend({
measurements,
}: {
measurements: MetricMeasurement[];
}) {
return (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Measurements</span>
<div className="flex flex-col gap-0.5">
{measurements.map((m, i) => (
<MeasurementRow key={i} measurement={m} />
))}
</div>
</div>
);
}

function MeasurementRow({ measurement }: { measurement: MetricMeasurement }) {
const parsed = parsePrometheusMeasurement(measurement.data);
const isPassed = measurement.status === "passed";
const isFailed = measurement.status === "failed";

const timeAgo = formatDistanceToNowStrict(
new Date(measurement.measuredAt),
{ addSuffix: true },
);

return (
<div className="flex items-center justify-between rounded px-1 py-0.5 text-xs">
<div className="flex items-center gap-2">
<span
className={cn(
"inline-block h-2 w-2 rounded-full",
isPassed && "bg-green-500",
isFailed && "bg-red-500",
!isPassed && !isFailed && "bg-muted-foreground",
)}
/>
<span className="text-muted-foreground">{timeAgo}</span>
</div>
<span className="font-mono">
{parsed?.value != null ? parsed.value : "—"}
</span>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function PrometheusIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 128 128"
fill="currentColor"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64 64-28.7 64-64S99.3 0 64 0zm0 119.9c-8.5 0-15.4-5.4-15.4-12.1h30.8c0 6.7-6.9 12.1-15.4 12.1zm25.4-16.3H38.6v-9.2h50.8v9.2zm-.3-13.8H38.8c-.1-.2-.3-.3-.4-.5-4.5-6-5.9-9.1-7.4-12.1-.1-.1 0-.3 0-.3 0 .1 8.4 4 16.6 4 4.4 0 7.7-.5 10.4-1.2-9.8-2-16.9-5.9-16.9-5.9s1.8 1.1 4.8 2.2c-4.4-2.5-4.6-6.6-4.6-6.6s5.2 3.7 12.8 5.1c-3.8-1.6-6.7-4.3-6.7-4.3s3.6 1.4 10.3 2.5c-3.3-1.8-5.1-4.3-5.1-4.3s8.3 3.3 17.3 3.3c2.1 0 3.6-.1 4.8-.3-1.1-1.1-2.6-3.1-2.6-3.1s6.3 1.6 12 1.6c1.7 0 3-.1 4.2-.3-1.2-1.5-2-3.2-2-3.2s5.9 2.1 11 2.1c1 0 1.8-.1 2.5-.1-4.2-5.7-6.7-13.1-6.7-13.1s1.1 1.4 2.9 3.3c-1.1-3.8-1.1-8.7-1.1-8.7s1.2 2.7 3.1 5.5c-.4-4.4.6-10.4.6-10.4s.8 3.2 2.2 6.4c.8-4.7 3.5-9.5 3.5-9.5s.1 4.7 1 8.6c1.6-3.8 4.6-7.6 4.6-7.6s-1 5.7-1.1 9.1c2.2-3.2 5.8-6.2 5.8-6.2s-2.2 5.1-3 8.5c2.6-2.3 7.2-4.8 7.2-4.8s-3.9 5.2-5.3 8.2c6.8-3.3 7.5-3.7 7.5-3.7s-.2.1-.3.2c-1.6 1.6-11.2 8.3-12.7 9.8-.5.5-.8 1.1-1 1.7-1.5 4.3-3.3 7.5-7.6 13.3z" />
</svg>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";

export const prometheusProviderConfig = z.object({
type: z.literal("prometheus"),
address: z.string(),
query: z.string(),
timeout: z.number().optional(),
});

export type PrometheusProviderConfig = z.infer<typeof prometheusProviderConfig>;

const prometheusResultEntry = z.object({
metric: z.record(z.string()).optional(),
value: z.number(),
});

export const prometheusMeasurementData = z.object({
ok: z.boolean(),
statusCode: z.number(),
duration: z.number().optional(),
value: z.number().nullable().optional(),
results: z.array(prometheusResultEntry).nullable().optional(),
error: z.string().optional(),
errorType: z.string().optional(),
});

export type PrometheusMeasurementData = z.infer<
typeof prometheusMeasurementData
>;

export function parsePrometheusProvider(
provider: unknown,
): PrometheusProviderConfig | null {
const result = prometheusProviderConfig.safeParse(provider);
return result.success ? result.data : null;
}

export function parsePrometheusMeasurement(
data: unknown,
): PrometheusMeasurementData | null {
const result = prometheusMeasurementData.safeParse(data);
return result.success ? result.data : null;
}

export function isPrometheusMeasurement(data: unknown): boolean {
return prometheusMeasurementData.safeParse(data).success;
}

export function isPrometheusProvider(provider: unknown): boolean {
return prometheusProviderConfig.safeParse(provider).success;
}
Loading