Skip to content

Commit

Permalink
feat: new emui service logs (#1702)
Browse files Browse the repository at this point in the history
## Description:
This change adds support for showing enclave service logs in the new
enclave manager ui.

Additionally, this pr contains a partial implementation of the service
view ui - full implementation to follow.

### Demo:

In this demo `github.com/kurtosis-tech/log-load-package` is used to
demonstrate large quantities of logs don't seem to break following.
Additionally, my browser has it's locale set to san francisco - so that
browser locale dependent timestamps can be demonstrated.


https://github.com/kurtosis-tech/kurtosis/assets/4419574/533b5f5a-7fdf-40b1-a6c5-e0b67970811c

## Is this change user facing?
YES
  • Loading branch information
Dartoxian committed Nov 7, 2023
1 parent edbfcba commit cedeed1
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 30 deletions.
34 changes: 29 additions & 5 deletions enclave-manager/web/src/client/enclaveManager/KurtosisClient.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { PromiseClient } from "@connectrpc/connect";
import { RunStarlarkPackageArgs } from "enclave-manager-sdk/build/api_container_service_pb";
import { RunStarlarkPackageArgs, ServiceInfo } from "enclave-manager-sdk/build/api_container_service_pb";
import {
CreateEnclaveArgs,
DestroyEnclaveArgs,
EnclaveAPIContainerInfo,
EnclaveInfo,
EnclaveMode,
GetServiceLogsArgs,
LogLineFilter,
} from "enclave-manager-sdk/build/engine_service_pb";
import { KurtosisEnclaveManagerServer } from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_connect";
import {
Expand All @@ -14,8 +16,9 @@ import {
GetStarlarkRunRequest,
RunStarlarkPackageRequest,
} from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_pb";
import { assertDefined, asyncResult } from "../../utils";
import { assertDefined, asyncResult, isDefined } from "../../utils";
import { RemoveFunctions } from "../../utils/types";
import { EnclaveFullInfo } from "../../emui/enclaves/types";

export abstract class KurtosisClient {
protected readonly client: PromiseClient<typeof KurtosisEnclaveManagerServer>;
Expand Down Expand Up @@ -80,7 +83,28 @@ export abstract class KurtosisClient {
apicPort: apicInfo.grpcPortInsideEnclave,
});
return this.client.getServices(request, this.getHeaderOptions());
}, "KurtosisClient could not getServices");
}, `KurtosisClient could not getServices for ${enclave.name}`);
}

async getServiceLogs(
abortController: AbortController,
enclave: RemoveFunctions<EnclaveFullInfo>,
services: ServiceInfo[],
followLogs?: boolean,
numLogLines?: number,
returnAllLogs?: boolean,
conjunctiveFilters: LogLineFilter[] = [],
) {
// Not currently using asyncResult as the return type here is an asyncIterable
const request = new GetServiceLogsArgs({
enclaveIdentifier: enclave.name,
serviceUuidSet: services.reduce((acc, service) => ({ ...acc, [service.serviceUuid]: true }), {}),
followLogs: isDefined(followLogs) ? followLogs : true,
conjunctiveFilters: conjunctiveFilters,
numLogLines: isDefined(numLogLines) ? numLogLines : 1500,
returnAllLogs: !!returnAllLogs,
});
return this.client.getServiceLogs(request, { ...this.getHeaderOptions(), signal: abortController.signal });
}

async getStarlarkRun(enclave: RemoveFunctions<EnclaveInfo>) {
Expand All @@ -95,7 +119,7 @@ export abstract class KurtosisClient {
apicPort: apicInfo.grpcPortInsideEnclave,
});
return this.client.getStarlarkRun(request, this.getHeaderOptions());
}, "KurtosisClient could not getStarlarkRun");
}, `KurtosisClient could not getStarlarkRun for ${enclave.name}`);
}

async listFilesArtifactNamesAndUuids(enclave: RemoveFunctions<EnclaveInfo>) {
Expand All @@ -110,7 +134,7 @@ export abstract class KurtosisClient {
apicPort: apicInfo.grpcPortInsideEnclave,
});
return this.client.listFilesArtifactNamesAndUuids(request, this.getHeaderOptions());
}, "KurtosisClient could not listFilesArtifactNamesAndUuids");
}, `KurtosisClient could not listFilesArtifactNamesAndUuids for ${enclave.name}`);
}

async createEnclave(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { KurtosisClient } from "./KurtosisClient";

export class LocalKurtosisClient extends KurtosisClient {
constructor() {
const defaultUrl = new URL(window.location.href);
const defaultUrl = new URL(`${window.location.protocol}//${window.location.host}`);
super(
createPromiseClient(
KurtosisEnclaveManagerServer,
Expand Down
13 changes: 11 additions & 2 deletions enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,27 @@ export type KurtosisBreadcrumb = {
export const KurtosisBreadcrumbs = () => {
const matches = useMatches() as UIMatch<
object,
{ crumb?: (data: object, params: Params<string>) => KurtosisBreadcrumb | Promise<KurtosisBreadcrumb> }
{
crumb?: (
data: Record<string, object>,
params: Params<string>,
) => KurtosisBreadcrumb | Promise<KurtosisBreadcrumb>;
}
>[];

const [matchCrumbs, setMatchCrumbs] = useState<KurtosisBreadcrumb[]>([]);

useEffect(() => {
(async () => {
const allLoaderData = matches
.filter((match) => isDefined(match.data))
.reduce((acc, match) => ({ ...acc, [match.id]: match.data }), {});

setMatchCrumbs(
await Promise.all(
matches
.map((match) =>
isDefined(match.handle?.crumb) ? Promise.resolve(match.handle.crumb(match.data, match.params)) : null,
isDefined(match.handle?.crumb) ? Promise.resolve(match.handle.crumb(allLoaderData, match.params)) : null,
)
.filter(isDefined),
),
Expand Down
47 changes: 31 additions & 16 deletions enclave-manager/web/src/components/enclaves/logs/LogLine.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Box } from "@chakra-ui/react";
import { Box, Flex } from "@chakra-ui/react";
import { DateTime } from "luxon";
import { isDefined } from "../../../utils";

export type LogStatus = "info" | "error";

export type LogLineProps = {
timestamp?: DateTime;
message?: string;
status?: LogStatus;
};

export const LogLine = ({ message, status }: LogLineProps) => {
export const LogLine = ({ timestamp, message, status }: LogLineProps) => {
const statusToColor = (status?: LogStatus) => {
switch (status) {
case "error":
Expand All @@ -20,19 +23,31 @@ export const LogLine = ({ message, status }: LogLineProps) => {
};

return (
<Box
as={"pre"}
whiteSpace={"pre-wrap"}
borderBottom={"1px solid #444444"}
p={"14px 0"}
m={"0 16px"}
fontSize={"xs"}
lineHeight="2"
fontWeight={400}
fontFamily="Ubuntu Mono"
color={statusToColor(status)}
>
{message || <i>No message</i>}
</Box>
<Flex borderBottom={"1px solid #444444"} p={"14px 0"} m={"0 16px"} gap={"8px"} alignItems={"center"}>
{isDefined(timestamp) && (
<Box
as={"pre"}
whiteSpace={"pre-wrap"}
fontSize={"xs"}
lineHeight="2"
fontWeight={700}
fontFamily="Ubuntu Mono"
color={"white"}
>
{timestamp.toLocal().toFormat("yyyy-MM-dd hh:mm:ss.SSS ZZZZ")}
</Box>
)}
<Box
as={"pre"}
whiteSpace={"pre-wrap"}
fontSize={"xs"}
lineHeight="2"
fontWeight={400}
fontFamily="Ubuntu Mono"
color={statusToColor(status)}
>
{message || <i>No message</i>}
</Box>
</Flex>
);
};
12 changes: 11 additions & 1 deletion enclave-manager/web/src/components/enclaves/logs/LogViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const LogViewer = ({
}: LogViewerProps) => {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const [logLines, setLogLines] = useState(propsLogLines);
const [userIsScrolling, setUserIsScrolling] = useState(false);
const [automaticScroll, setAutomaticScroll] = useState(true);

const throttledSetLogLines = useMemo(() => throttle(setLogLines, 500), []);
Expand All @@ -37,6 +38,14 @@ export const LogViewer = ({
}
};

const handleBottomStateChange = (atBottom: boolean) => {
if (userIsScrolling) {
setAutomaticScroll(atBottom);
} else if (automaticScroll && !atBottom) {
virtuosoRef.current?.scrollToIndex({ index: "LAST" });
}
};

const getLogsValue = () => {
return logLines
.map(({ message }) => message)
Expand Down Expand Up @@ -71,7 +80,8 @@ export const LogViewer = ({
<Virtuoso
ref={virtuosoRef}
followOutput={automaticScroll}
atBottomStateChange={(atBottom) => setAutomaticScroll(atBottom)}
atBottomStateChange={handleBottomStateChange}
isScrolling={setUserIsScrolling}
style={{ height: "660px" }}
data={logLines.filter(({ message }) => isDefined(message))}
itemContent={(index, line) => <LogLine {...line} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type ServicesTableRow = {

const serviceToRow = (service: ServiceInfo): ServicesTableRow => {
return {
serviceUUID: service.serviceUuid,
serviceUUID: service.shortenedUuid,
name: service.name,
status: service.serviceStatus,
image: service.container?.imageName,
Expand Down
51 changes: 47 additions & 4 deletions enclave-manager/web/src/emui/enclaves/EnclaveRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { enclavesAction } from "./action";
import { Enclave, enclaveLoader, enclaveTabLoader } from "./enclave";
import { runStarlarkAction } from "./enclave/action";
import { EnclaveLoaderDeferred } from "./enclave/loader";
import { Service } from "./enclave/service/Service";
import { EnclaveList } from "./EnclaveList";
import { enclavesLoader } from "./loader";
import { serviceTabLoader } from "./enclave/service/tabLoader";

export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => [
{
Expand All @@ -25,8 +27,8 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] =>
loader: enclaveLoader(kurtosisClient),
id: "enclave",
handle: {
crumb: async (data: EnclaveLoaderDeferred, params: Params) => {
const resolvedData = await data.data;
crumb: async (data: Record<string, object>, params: Params) => {
const resolvedData = await (data["enclave"] as EnclaveLoaderDeferred).data;
return {
name: resolvedData.routeName,
destination: `/enclave/${params.enclaveUUID}`,
Expand All @@ -36,6 +38,46 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] =>
children: [
{
path: "service/:serviceUUID",
handle: {
crumb: async (data: Record<string, object>, params: Params) => {
const resolvedData = await (data["enclave"] as EnclaveLoaderDeferred).data;
let serviceName = "Unknown";
if (
resolvedData.enclave &&
resolvedData.enclave.isOk &&
resolvedData.enclave.value.services.isOk &&
params.serviceUUID
) {
const service = Object.values(resolvedData.enclave.value.services.value.serviceInfo).find(
(service) => service.shortenedUuid === params.serviceUUID,
);
if (service) {
serviceName = service.name;
}
}

return {
name: serviceName,
destination: `/enclave/${params.enclaveUUID}/service/${params.serviceUUID}`,
};
},
},
children: [
{
path: ":activeTab?",
loader: serviceTabLoader,
id: "serviceActiveTab",
element: <Service />,
handle: {
crumb: (data: Record<string, object>, params: Params<string>) => ({
name: (data["serviceActiveTab"] as Awaited<ReturnType<typeof serviceTabLoader>>).routeName,
destination: `/enclave/${params.enclaveUUID}/service/${params.serviceUUID}/${
params.activeTab || "overview"
}`,
}),
},
},
],
},
{
path: "file/:fileUUID",
Expand All @@ -44,10 +86,11 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] =>
path: ":activeTab?",
loader: enclaveTabLoader,
action: runStarlarkAction(kurtosisClient),
id: "enclaveActiveTab",
element: <Enclave />,
handle: {
crumb: (data: Awaited<ReturnType<typeof enclaveTabLoader>>, params: Params<string>) => ({
name: data.routeName,
crumb: (data: Record<string, object>, params: Params<string>) => ({
name: (data["enclaveActiveTab"] as Awaited<ReturnType<typeof enclaveTabLoader>>).routeName,
destination: `/enclave/${params.enclaveUUID}/${params.activeTab || "overview"}`,
}),
},
Expand Down
Loading

0 comments on commit cedeed1

Please sign in to comment.