Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docker/dev-full/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ services:
# We only reserve 100 ports instead of the default 22,000. See
# rivet-guard for explanation.
- "7600-7699:7600-7699"
# cAdvisor metrics endpoint
- "7780:7780"
networks:
- rivet-network

Expand Down
10 changes: 9 additions & 1 deletion docker/dev-full/otel-collector/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ receivers:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: 'cadvisor'
static_configs:
- targets: ['rivet-client:7780']
metrics_path: /metrics
scrape_interval: 30s

processors:
batch:
Expand Down Expand Up @@ -52,7 +60,7 @@ service:
processors: [batch]
exporters: [clickhouse]
metrics:
receivers: [otlp]
receivers: [otlp, prometheus]
processors: [batch]
exporters: [clickhouse]

19 changes: 17 additions & 2 deletions docker/universal/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ RUN apt-get update -y && \
apt-get install -y ca-certificates openssl curl tini curl && \
curl -Lf -o /lib/libfdb_c.so "https://github.com/apple/foundationdb/releases/download/7.1.60/libfdb_c.x86_64.so"

# MARK: Runner (Full)
FROM --platform=linux/amd64 base-runner AS client-full
# MARK: Runner (Slim)
FROM --platform=linux/amd64 base-runner AS client-slim
ARG CNI_PLUGINS_VERSION=1.3.0
RUN apt-get install -y skopeo iproute2 runc dnsutils && \
echo "Downloading lz4" && \
Expand All @@ -142,6 +142,21 @@ COPY ./docker/dev-full/rivet-client/rivet-actor.conflist /opt/cni/config/rivet-a
COPY --from=builder /app/dist/rivet-client /app/dist/rivet-container-runner /usr/local/bin/
ENTRYPOINT ["/usr/bin/tini", "--", "entrypoint.sh"]

# MARK: Runner (Full)
FROM client-slim AS client-full
ARG CADVISOR_VERSION=v0.52.0
RUN apt-get update -y && \
apt-get install -y wget && \
wget -O /usr/local/bin/cadvisor "https://github.com/google/cadvisor/releases/download/${CADVISOR_VERSION}/cadvisor-${CADVISOR_VERSION}-linux-amd64" && \
chmod +x /usr/local/bin/cadvisor && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

COPY docker/universal/client-full-entrypoint.sh /usr/local/bin/client-full-entrypoint.sh
RUN chmod +x /usr/local/bin/client-full-entrypoint.sh

ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/client-full-entrypoint.sh"]

# MARK: Monlith
FROM --platform=linux/amd64 debian:12.9-slim AS monolith
ENV DEBIAN_FRONTEND=noninteractive
Expand Down
17 changes: 17 additions & 0 deletions docker/universal/client-full-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
set -e

# Start cadvisor in the background
cadvisor \
--port=7780 \
--listen_ip=0.0.0.0 \
--prometheus_endpoint="/metrics" \
--enable_metrics=cpu,cpuLoad,memory,network,disk,diskIO,oom_event,process,tcp,udp \
--docker_only=false \
--disable_root_cgroup_stats=false &

# TODO:
# --raw_cgroup_prefix_whitelist="" \

# Start rivet-client with all passed arguments
exec rivet-client "$@"
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
actorBuildsAtom,
createActorAtom,
type Logs,
type Metrics,
actorsQueryAtom,
actorsInternalFilterAtom,
type Actor,
Expand All @@ -30,6 +31,7 @@ import {
createActorEndpoint,
destroyActorMutationOptions,
actorLogsQueryOptions,
actorMetricsQueryOptions,
actorRegionsQueryOptions,
actorBuildsQueryOptions,
} from "../../queries";
Expand Down Expand Up @@ -257,9 +259,63 @@ export function ActorsProvider({
};
};

const metrics = atom({
metrics: { cpu: null, memory: null } as Metrics,
status: "pending",
});
metrics.onMount = (set) => {
const metricsObserver = new QueryObserver(
queryClient,
actorMetricsQueryOptions({
projectNameId,
environmentNameId,
actorId: actor.id,
}, { refetchInterval: 5000 }),
);

type MetricsQuery = {
status: string;
data?: Awaited<
ReturnType<
Exclude<
ReturnType<
typeof actorMetricsQueryOptions
>["queryFn"],
undefined
>
>
>;
};

function updateMetrics(query: MetricsQuery) {
const data = query.data;
set((prev) => ({
...prev,
...data,
status: query.status,
}));
}

const subMetrics = metricsObserver.subscribe(
(query) => {
updateMetrics(query);
},
);

updateMetrics(
metricsObserver.getCurrentQuery().state,
);

return () => {
metricsObserver.destroy();
subMetrics();
};
};

return {
...actor,
logs,
metrics,
destroy,
status: getActorStatus(actor),
};
Expand Down
132 changes: 128 additions & 4 deletions frontend/apps/hub/src/domains/project/queries/actors/query-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
keepPreviousData,
queryOptions,
} from "@tanstack/react-query";
import stripAnsi from 'strip-ansi';
import stripAnsi from "strip-ansi";

export const projectActorsQueryOptions = ({
projectNameId,
Expand Down Expand Up @@ -243,18 +243,142 @@ export const actorLogsQueryOptions = (
line: raw,
message: "",
properties: {},
} as const
} as const;
});


return {...response, logs };
return { ...response, logs };
},
meta: {
watch: mergeWatchStreams,
},
});
};

export const actorMetricsQueryOptions = (
{
projectNameId,
environmentNameId,
actorId,
}: {
projectNameId: string;
environmentNameId: string;
actorId: string;
},
opts: { refetchInterval?: number } = {},
) => {
return queryOptions({
...opts,
queryKey: [
"project",
projectNameId,
"environment",
environmentNameId,
"actor",
actorId,
"metrics",
] as const,
queryFn: async ({
signal: abortSignal,
queryKey: [, project, , environment, , actorId],
}) => {
const pollOffset = 5_000;
const pollInterval = 15_000;

const now = Date.now();
const start = now - pollInterval * 2 - pollOffset; // Last minute + 2 data points
const end = now - pollOffset; // Metrics have a minimum a 5 second latency based on poll interval

const response = await rivetClient.actors.metrics.get(
actorId,
{
project,
environment,
start,
end,
interval: pollInterval,
},
{ abortSignal },
);

// Process the new response format
const metrics: Record<string, number | null> = {};
const rawData: Record<string, number[]> = {};

if (
response.metricNames &&
response.metricValues &&
response.metricAttributes &&
response.metricNames.length > 0
) {
response.metricNames.forEach((metricName, index) => {
const metricValues = response.metricValues[index];
const attributes = response.metricAttributes[index] || {};

// Create the metric key based on the metric name and attributes
let metricKey = metricName;

// Handle specific attribute mappings to match UI expectations
if (attributes.failure_type && attributes.scope) {
metricKey = `memory_failures_${attributes.failure_type}_${attributes.scope}`;
} else if (attributes.tcp_state) {
if (metricName.includes('tcp6')) {
metricKey = `network_tcp6_usage_${attributes.tcp_state}`;
} else {
metricKey = `network_tcp_usage_${attributes.tcp_state}`;
}
} else if (attributes.udp_state) {
if (metricName.includes('udp6')) {
metricKey = `network_udp6_usage_${attributes.udp_state}`;
} else {
metricKey = `network_udp_usage_${attributes.udp_state}`;
}
} else if (attributes.state) {
metricKey = `tasks_state_${attributes.state}`;
} else if (attributes.interface) {
// Handle network interface attributes
const baseMetric = metricName.replace(/^container_/, '');
metricKey = `${baseMetric}_${attributes.interface}`;
} else if (attributes.device) {
// Handle filesystem device attributes
const baseMetric = metricName.replace(/^container_/, '');
metricKey = `${baseMetric}_${attributes.device}`;
} else {
// Remove "container_" prefix to match UI expectations
metricKey = metricName.replace(/^container_/, '');
}

// Store raw time series data for rate calculations
rawData[metricKey] = metricValues || [];

if (metricValues && metricValues.length > 0) {
// Get the latest non-zero value (last value is often 0)
let value = null;
for (let i = metricValues.length - 1; i >= 0; i--) {
if (metricValues[i] !== 0) {
value = metricValues[i];
break;
}
}
// If all values are 0, use the last value anyway
if (value === null && metricValues.length > 0) {
value = metricValues[metricValues.length - 1];
}
metrics[metricKey] = value;
} else {
metrics[metricKey] = null;
}
});
}

return {
metrics,
rawData,
interval: pollInterval,
};
},
});
};

export const actorBuildsQueryOptions = ({
projectNameId,
environmentNameId,
Expand Down
10 changes: 10 additions & 0 deletions frontend/packages/components/src/actors/actor-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ActorFeature {
State = "state",
Console = "console",
Runtime = "runtime",
Metrics = "metrics",
InspectReconnectNotification = "inspect_reconnect_notification",
}

Expand All @@ -25,6 +26,7 @@ export type Actor = Omit<
lifecycle?: Rivet.actor.Lifecycle;
endpoint?: string;
logs: LogsAtom;
metrics: MetricsAtom;
network?: Rivet.actor.Network | null;
resources?: Rivet.actor.Resources | null;
runtime?: Rivet.actor.Runtime | null;
Expand All @@ -43,6 +45,8 @@ export type Logs = {
properties: Record<string, unknown>;
}[];

export type Metrics = Record<string, number | null>;

export type Build = Rivet.actor.Build;
export type DestroyActor = {
isDestroying: boolean;
Expand All @@ -55,6 +59,11 @@ export type LogsAtom = Atom<{
// query status
status: string;
}>;
export type MetricsAtom = Atom<{
metrics: Metrics;
// query status
status: string;
}>;
export type BuildAtom = Atom<Build>;
export type DestroyActorAtom = Atom<DestroyActor>;

Expand Down Expand Up @@ -378,6 +387,7 @@ const commonActorFeatures = [
ActorFeature.Logs,
ActorFeature.Config,
ActorFeature.Runtime,
ActorFeature.Metrics,
ActorFeature.InspectReconnectNotification,
];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Badge, Button, LiveBadge, WithTooltip } from "@rivet-gg/components";
import { Badge, Button, WithTooltip } from "@rivet-gg/components";
import {
type CodeMirrorRef,
EditorView,
Expand Down Expand Up @@ -42,8 +42,6 @@ export function ActorEditableState({ state }: ActorEditableStateProps) {
<>
<div className="flex justify-between items-center border-b gap-1 p-2">
<div className="flex items-center justify-start gap-1">
<LiveBadge />

<ActorStateChangeIndicator state={state.value} />
</div>
<div className="flex gap-2">
Expand Down
Loading
Loading