Skip to content

Commit 85ab9c2

Browse files
chore(site): replace agent log service (#9814)
1 parent 7bb0061 commit 85ab9c2

File tree

6 files changed

+114
-192
lines changed

6 files changed

+114
-192
lines changed

site/src/api/api.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ export const watchBuildLogsByTemplateVersionId = (
13921392
type WatchWorkspaceAgentLogsOptions = {
13931393
after: number;
13941394
onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void;
1395-
onDone: () => void;
1395+
onDone?: () => void;
13961396
onError: (error: Error) => void;
13971397
};
13981398

@@ -1423,7 +1423,7 @@ export const watchWorkspaceAgentLogs = (
14231423
onError(new Error("socket errored"));
14241424
});
14251425
socket.addEventListener("close", () => {
1426-
onDone();
1426+
onDone && onDone();
14271427
});
14281428

14291429
return socket;

site/src/components/Resources/AgentRow.stories.tsx

+16-13
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
MockWorkspaceApp,
1818
MockProxyLatencies,
1919
} from "testHelpers/entities";
20-
import { AgentRow } from "./AgentRow";
20+
import { AgentRow, LineWithID } from "./AgentRow";
2121
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
2222
import type { Meta, StoryObj } from "@storybook/react";
2323

@@ -84,24 +84,27 @@ const defaultAgentMetadata = [
8484
},
8585
];
8686

87+
const storybookLogs: LineWithID[] = [
88+
"\x1b[91mCloning Git repository...",
89+
"\x1b[2;37;41mStarting Docker Daemon...",
90+
"\x1b[1;95mAdding some 🧙magic🧙...",
91+
"Starting VS Code...",
92+
"\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r100 1475 0 1475 0 0 4231 0 --:--:-- --:--:-- --:--:-- 4238",
93+
].map((line, index) => ({
94+
id: index,
95+
level: "info",
96+
output: line,
97+
time: "",
98+
}));
99+
87100
const meta: Meta<typeof AgentRow> = {
88101
title: "components/AgentRow",
89102
component: AgentRow,
90103
args: {
91-
storybookLogs: [
92-
"\x1b[91mCloning Git repository...",
93-
"\x1b[2;37;41mStarting Docker Daemon...",
94-
"\x1b[1;95mAdding some 🧙magic🧙...",
95-
"Starting VS Code...",
96-
"\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r100 1475 0 1475 0 0 4231 0 --:--:-- --:--:-- --:--:-- 4238",
97-
].map((line, index) => ({
98-
id: index,
99-
level: "info",
100-
output: line,
101-
time: "",
102-
})),
104+
storybookLogs,
103105
agent: {
104106
...MockWorkspaceAgent,
107+
logs_length: storybookLogs.length,
105108
startup_script:
106109
'set -eux -o pipefail\n\n# install and start code-server\ncurl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3\n/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &\n\n\nif [ ! -d ~/coder ]; then\n mkdir -p ~/coder\n\n git clone https://github.com/coder/coder ~/coder\nfi\n\nsudo service docker start\nDOTFILES_URI=" "\nrm -f ~/.personalize.log\nif [ -n "${DOTFILES_URI// }" ]; then\n coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log\nfi\nif [ -x ~/personalize ]; then\n ~/personalize 2>&1 | tee -a ~/.personalize.log\nelif [ -f ~/personalize ]; then\n echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log\nfi\n',
107110
},

site/src/components/Resources/AgentRow.tsx

+73-44
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import Popover from "@mui/material/Popover";
22
import { makeStyles, useTheme } from "@mui/styles";
33
import Skeleton from "@mui/material/Skeleton";
4-
import { useMachine } from "@xstate/react";
4+
import * as API from "api/api";
55
import CodeOutlined from "@mui/icons-material/CodeOutlined";
66
import {
77
CloseDropdown,
88
OpenDropdown,
99
} from "components/DropdownArrows/DropdownArrows";
10-
import { LogLine, logLineHeight } from "components/WorkspaceBuildLogs/Logs";
10+
import {
11+
Line,
12+
LogLine,
13+
logLineHeight,
14+
} from "components/WorkspaceBuildLogs/Logs";
1115
import { PortForwardButton } from "./PortForwardButton";
1216
import { VSCodeDesktopButton } from "components/Resources/VSCodeDesktopButton/VSCodeDesktopButton";
1317
import {
@@ -25,15 +29,11 @@ import AutoSizer from "react-virtualized-auto-sizer";
2529
import { FixedSizeList as List, ListOnScrollProps } from "react-window";
2630
import { colors } from "theme/colors";
2731
import { combineClasses } from "utils/combineClasses";
28-
import {
29-
LineWithID,
30-
workspaceAgentLogsMachine,
31-
} from "xServices/workspaceAgentLogs/workspaceAgentLogsXService";
3232
import {
3333
Workspace,
3434
WorkspaceAgent,
3535
WorkspaceAgentMetadata,
36-
} from "../../api/typesGenerated";
36+
} from "api/typesGenerated";
3737
import { AppLink } from "./AppLink/AppLink";
3838
import { SSHButton } from "./SSHButton/SSHButton";
3939
import { Stack } from "../Stack/Stack";
@@ -44,6 +44,14 @@ import { AgentVersion } from "./AgentVersion";
4444
import { AgentStatus } from "./AgentStatus";
4545
import Collapse from "@mui/material/Collapse";
4646
import { useProxy } from "contexts/ProxyContext";
47+
import { displayError } from "components/GlobalSnackbar/utils";
48+
49+
// Logs are stored as the Line interface to make rendering
50+
// much more efficient. Instead of mapping objects each time, we're
51+
// able to just pass the array of logs to the component.
52+
export interface LineWithID extends Line {
53+
id: number;
54+
}
4755

4856
export interface AgentRowProps {
4957
agent: WorkspaceAgent;
@@ -68,24 +76,11 @@ export const AgentRow: FC<AgentRowProps> = ({
6876
hideVSCodeDesktopButton,
6977
serverVersion,
7078
onUpdateAgent,
71-
storybookLogs,
7279
storybookAgentMetadata,
7380
sshPrefix,
81+
storybookLogs,
7482
}) => {
7583
const styles = useStyles();
76-
const [logsMachine, sendLogsEvent] = useMachine(workspaceAgentLogsMachine, {
77-
context: { agentID: agent.id },
78-
services: process.env.STORYBOOK
79-
? {
80-
getLogs: async () => {
81-
return storybookLogs || [];
82-
},
83-
streamLogs: () => async () => {
84-
// noop
85-
},
86-
}
87-
: undefined,
88-
});
8984
const theme = useTheme();
9085
const startupScriptAnchorRef = useRef<HTMLButtonElement>(null);
9186
const [startupScriptOpen, setStartupScriptOpen] = useState(false);
@@ -94,36 +89,20 @@ export const AgentRow: FC<AgentRowProps> = ({
9489
showApps &&
9590
((agent.status === "connected" && hasAppsToDisplay) ||
9691
agent.status === "connecting");
97-
const hasStartupFeatures =
98-
Boolean(agent.logs_length) || Boolean(logsMachine.context.logs?.length);
92+
const hasStartupFeatures = Boolean(agent.logs_length);
9993
const { proxy } = useProxy();
100-
10194
const [showLogs, setShowLogs] = useState(
10295
["starting", "start_timeout"].includes(agent.lifecycle_state) &&
10396
hasStartupFeatures,
10497
);
105-
useEffect(() => {
106-
setShowLogs(agent.lifecycle_state !== "ready" && hasStartupFeatures);
107-
}, [agent.lifecycle_state, hasStartupFeatures]);
108-
// External applications can provide startup logs for an agent during it's spawn.
109-
// These could be Kubernetes logs, or other logs that are useful to the user.
110-
// For this reason, we want to fetch these logs when the agent is starting.
111-
useEffect(() => {
112-
if (agent.lifecycle_state === "starting") {
113-
sendLogsEvent("FETCH_LOGS");
114-
}
115-
}, [sendLogsEvent, agent.lifecycle_state]);
116-
useEffect(() => {
117-
// We only want to fetch logs when they are actually shown,
118-
// otherwise we can make a lot of requests that aren't necessary.
119-
if (showLogs && logsMachine.can("FETCH_LOGS")) {
120-
sendLogsEvent("FETCH_LOGS");
121-
}
122-
}, [logsMachine, sendLogsEvent, showLogs]);
98+
const agentLogs = useAgentLogs(agent.id, {
99+
enabled: showLogs,
100+
initialData: process.env.STORYBOOK ? storybookLogs || [] : undefined,
101+
});
123102
const logListRef = useRef<List>(null);
124103
const logListDivRef = useRef<HTMLDivElement>(null);
125104
const startupLogs = useMemo(() => {
126-
const allLogs = logsMachine.context.logs || [];
105+
const allLogs = agentLogs || [];
127106

128107
const logs = [...allLogs];
129108
if (agent.logs_overflowed) {
@@ -135,8 +114,13 @@ export const AgentRow: FC<AgentRowProps> = ({
135114
});
136115
}
137116
return logs;
138-
}, [logsMachine.context.logs, agent.logs_overflowed]);
117+
}, [agentLogs, agent.logs_overflowed]);
139118
const [bottomOfLogs, setBottomOfLogs] = useState(true);
119+
120+
useEffect(() => {
121+
setShowLogs(agent.lifecycle_state !== "ready" && hasStartupFeatures);
122+
}, [agent.lifecycle_state, hasStartupFeatures]);
123+
140124
// This is a layout effect to remove flicker when we're scrolling to the bottom.
141125
useLayoutEffect(() => {
142126
// If we're currently watching the bottom, we always want to stay at the bottom.
@@ -396,6 +380,51 @@ export const AgentRow: FC<AgentRowProps> = ({
396380
);
397381
};
398382

383+
const useAgentLogs = (
384+
agentId: string,
385+
{ enabled, initialData }: { enabled: boolean; initialData?: LineWithID[] },
386+
) => {
387+
const [logs, setLogs] = useState<LineWithID[] | undefined>(initialData);
388+
const socket = useRef<WebSocket | null>(null);
389+
390+
useEffect(() => {
391+
if (!enabled) {
392+
socket.current?.close();
393+
return;
394+
}
395+
396+
socket.current = API.watchWorkspaceAgentLogs(agentId, {
397+
// Get all logs
398+
after: 0,
399+
onMessage: (logs) => {
400+
setLogs((previousLogs) => {
401+
const newLogs: LineWithID[] = logs.map((log) => ({
402+
id: log.id,
403+
level: log.level || "info",
404+
output: log.output,
405+
time: log.created_at,
406+
}));
407+
408+
if (!previousLogs) {
409+
return newLogs;
410+
}
411+
412+
return [...previousLogs, ...newLogs];
413+
});
414+
},
415+
onError: () => {
416+
displayError("Error on getting agent logs");
417+
},
418+
});
419+
420+
return () => {
421+
socket.current?.close();
422+
};
423+
}, [agentId, enabled]);
424+
425+
return logs;
426+
};
427+
399428
const useStyles = makeStyles((theme) => ({
400429
agentRow: {
401430
backgroundColor: theme.palette.background.paperLight,

site/src/components/WorkspaceBuildLogs/Logs.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,25 @@ export const LogLine: FC<{
6060
const output = useMemo(() => {
6161
return convert.toHtml(line.output.split(/\r/g).pop() as string);
6262
}, [line.output]);
63+
const isUsingLineNumber = number !== undefined;
6364

6465
return (
65-
<div className={combineClasses([styles.line, line.level])} style={style}>
66+
<div
67+
className={combineClasses([
68+
styles.line,
69+
line.level,
70+
isUsingLineNumber && styles.lineNumber,
71+
])}
72+
style={style}
73+
>
6674
{!hideTimestamp && (
6775
<>
68-
<span className={styles.time}>
76+
<span
77+
className={combineClasses([
78+
styles.time,
79+
isUsingLineNumber && styles.number,
80+
])}
81+
>
6982
{number ? number : dayjs(line.time).format(`HH:mm:ss.SSS`)}
7083
</span>
7184
<span className={styles.space} />
@@ -119,6 +132,9 @@ const useStyles = makeStyles((theme) => ({
119132
backgroundColor: theme.palette.warning.dark,
120133
},
121134
},
135+
lineNumber: {
136+
paddingLeft: theme.spacing(2),
137+
},
122138
space: {
123139
userSelect: "none",
124140
width: theme.spacing(3),
@@ -132,4 +148,8 @@ const useStyles = makeStyles((theme) => ({
132148
display: "inline-block",
133149
color: theme.palette.text.secondary,
134150
},
151+
number: {
152+
width: theme.spacing(4),
153+
textAlign: "right",
154+
},
135155
}));

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const renderWorkspacePage = async () => {
4343
jest
4444
.spyOn(api, "watchWorkspaceAgentLogs")
4545
.mockImplementation((_, options) => {
46-
options.onDone();
46+
options.onDone && options.onDone();
4747
return new WebSocket("");
4848
});
4949
renderWithAuth(<WorkspacePage />, {

0 commit comments

Comments
 (0)