Ver Fonte

[POR-1654] support revision numbers on logs (applyv2) (#3507)

Co-authored-by: David Townley <davidtownley@Davids-MacBook-Air.local>
Co-authored-by: Feroze Mohideen <feroze@porter.run>
d-g-town há 2 anos atrás
pai
commit
5c9bf5ce23

+ 66 - 0
dashboard/src/lib/hooks/useRevisionList.ts

@@ -0,0 +1,66 @@
+import { useQuery } from "@tanstack/react-query";
+import { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { z } from "zod";
+import {AppRevision, appRevisionValidator} from "../revisions/types";
+import {useLatestRevision} from "../../main/home/app-dashboard/app-view/LatestRevisionContext";
+
+export function useRevisionList(appName: string, deploymentTargetId: string) {
+  const { currentProject, currentCluster } = useContext(Context);
+  const {latestRevision} = useLatestRevision();
+
+  const [
+    revisionList,
+    setRevisionList,
+  ] = useState<AppRevision[]>([]);
+
+  if (currentProject == null || currentCluster == null) {
+    return [];
+  }
+
+  const {data} = useQuery(
+      ["listAppRevisions", currentProject.id, currentCluster.id, appName, deploymentTargetId, latestRevision],
+      async () => {
+        const res = await api.listAppRevisions(
+            "<token>",
+            {
+              deployment_target_id: deploymentTargetId,
+            },
+            {
+              project_id: currentProject.id,
+              cluster_id: currentCluster.id,
+              porter_app_name: appName,
+            }
+        );
+
+        const revisions = await z
+            .object({
+              app_revisions: z.array(appRevisionValidator),
+            })
+            .parseAsync(res.data);
+
+        return revisions;
+      }
+  );
+
+  useEffect(() => {
+    if (data) {
+      setRevisionList(data.app_revisions);
+    }
+  }, [data]);
+
+  return revisionList;
+}
+
+export function useRevisionIdToNumber(appName: string, deploymentTargetId: string) {
+    const revisionList = useRevisionList(appName, deploymentTargetId);
+    const revisionIdToNumber: Record<string, number> = Object.fromEntries(revisionList.map(r => ([r.id, r.revision_number ])))
+
+    return revisionIdToNumber;
+}
+
+export function useLatestRevisionNumber(appName: string, deploymentTargetId: string) {
+  const revisionList = useRevisionList(appName, deploymentTargetId);
+  return revisionList.map((revision) => revision.revision_number).reduce((a, b) => Math.max(a, b), 0)
+}

+ 1 - 0
dashboard/src/lib/revisions/types.ts

@@ -13,6 +13,7 @@ export const appRevisionValidator = z.object({
   ]),
   b64_app_proto: z.string(),
   revision_number: z.number(),
+  id: z.string(),
   created_at: z.string(),
   updated_at: z.string(),
 });

+ 2 - 1
dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx

@@ -14,7 +14,7 @@ import Button from "components/porter/Button";
 import { useLatestRevision } from "../LatestRevisionContext";
 
 const LogsTab: React.FC = () => {
-    const { projectId, clusterId, latestProto , deploymentTargetId} = useLatestRevision();
+    const { projectId, clusterId, latestProto , deploymentTargetId, latestRevision} = useLatestRevision();
 
     const appName = latestProto.name
     const serviceNames = Object.keys(latestProto.services)
@@ -27,6 +27,7 @@ const LogsTab: React.FC = () => {
                 appName={appName}
                 serviceNames={serviceNames}
                 deploymentTargetId={deploymentTargetId}
+                latestRevision={latestRevision}
             />
         </>
     );

+ 1 - 0
dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts

@@ -18,6 +18,7 @@ export interface PaginationInfo {
     nextCursor: string | null;
 }
 
+
 const rawLabelsValidator = z.object({
     porter_run_absolute_name: z.string().optional(),
     porter_run_app_id: z.string().optional(),

+ 85 - 7
dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx

@@ -26,6 +26,9 @@ import Button from "components/porter/Button";
 import { Service } from "../../new-app-flow/serviceTypes";
 import LogFilterContainer from "../../expanded-app/logs/LogFilterContainer";
 import StyledLogs from "../../expanded-app/logs/StyledLogs";
+import {z} from "zod";
+import {AppRevision, appRevisionValidator} from "lib/revisions/types";
+import {useLatestRevisionNumber, useRevisionIdToNumber} from "lib/hooks/useRevisionList";
 
 type Props = {
     projectId: number;
@@ -33,6 +36,7 @@ type Props = {
     appName: string;
     serviceNames: string[];
     deploymentTargetId: string;
+    latestRevision: AppRevision;
 };
 
 const Logs: React.FC<Props> = ({
@@ -41,6 +45,7 @@ const Logs: React.FC<Props> = ({
     appName,
     serviceNames,
     deploymentTargetId,
+    latestRevision,
 }) => {
     const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
     const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
@@ -56,11 +61,14 @@ const Logs: React.FC<Props> = ({
 
     const [selectedFilterValues, setSelectedFilterValues] = useState<Record<LogFilterName, string>>({
         service_name:  GenericLogFilter.getDefaultOption("service_name").value,
-        pod_name: "", // not supported yet
-        revision: "", // not supported yet
+        pod_name: "", // not supported
+        revision: GenericLogFilter.getDefaultOption("revision").value,
         output_stream: GenericLogFilter.getDefaultOption("output_stream").value,
     });
 
+    const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId)
+    const latestRevisionNumber = useLatestRevisionNumber(appName, deploymentTargetId)
+
     const isAgentVersionUpdated = (agentImage: string | undefined) => {
         if (agentImage == null) {
             return false;
@@ -93,6 +101,15 @@ const Logs: React.FC<Props> = ({
         return patch >= 7;
     }
 
+    const createVersionOptions = (number: number) => {
+        return Array.from({ length: number }, (_, index) => {
+            const version = index + 1;
+            const label = version === number ? `Version ${version} (latest)` : `Version ${version}`;
+            const value = version.toString();
+            return GenericFilterOption.of(label, value);
+        }).reverse().slice(0, 3);
+    }
+
     const [filters, setFilters] = useState<GenericLogFilter[]>([
         {
             name: "service_name",
@@ -108,13 +125,26 @@ const Logs: React.FC<Props> = ({
                 }));
             }
         },
+        {
+            name: "revision",
+            displayName: "Version",
+            default: GenericLogFilter.getDefaultOption("revision"),
+            options: createVersionOptions(latestRevisionNumber),
+            setValue: (value: string) => {
+                setSelectedFilterValues((s) => ({
+                    ...s,
+                    revision: value,
+                }));
+            }
+        },
         {
             name: "output_stream",
             displayName: "Output Stream",
             default: GenericLogFilter.getDefaultOption("output_stream"),
-            options: serviceNames.map(s => {
-                return GenericFilterOption.of(s, s)
-            }) ?? [],
+            options: [
+                GenericFilterOption.of('stdout', 'stdout'),
+                GenericFilterOption.of("stderr", "stderr"),
+            ],
             setValue: (value: string) => {
                 setSelectedFilterValues((s) => ({
                     ...s,
@@ -142,9 +172,56 @@ const Logs: React.FC<Props> = ({
         enteredSearchText,
         notify,
         setIsLoading,
+        revisionIdToNumber,
         selectedDate,
     );
 
+    useEffect(() => {
+        setFilters([
+            {
+                name: "service_name",
+                displayName: "Service",
+                default: GenericLogFilter.getDefaultOption("service_name"),
+                options: serviceNames.map(s => {
+                    return GenericFilterOption.of(s, s)
+                }) ?? [],
+                setValue: (value: string) => {
+                    setSelectedFilterValues((s) => ({
+                        ...s,
+                        service_name: value,
+                    }));
+                }
+            },
+            {
+                name: "revision",
+                displayName: "Version",
+                default: GenericLogFilter.getDefaultOption("revision"),
+                options: createVersionOptions(latestRevisionNumber),
+                setValue: (value: string) => {
+                    setSelectedFilterValues((s) => ({
+                        ...s,
+                        revision: value,
+                    }));
+                }
+            },
+            {
+                name: "output_stream",
+                displayName: "Output Stream",
+                default: GenericLogFilter.getDefaultOption("output_stream"),
+                options: [
+                    GenericFilterOption.of('stdout', 'stdout'),
+                    GenericFilterOption.of("stderr", "stderr"),
+                ],
+                setValue: (value: string) => {
+                    setSelectedFilterValues((s) => ({
+                        ...s,
+                        output_stream: value,
+                    }));
+                }
+            },
+        ])
+    }, [latestRevisionNumber]);
+
     useEffect(() => {
         if (!isLoading && scrollToBottomRef.current && scrollToBottomEnabled) {
             const scrollPosition = scrollToBottomRef.current.offsetTop + scrollToBottomRef.current.offsetHeight - window.innerHeight;
@@ -159,8 +236,8 @@ const Logs: React.FC<Props> = ({
     const resetFilters = () => {
         setSelectedFilterValues({
             output_stream: GenericLogFilter.getDefaultOption("output_stream").value,
-            revision: "", // not supported yet
-            pod_name: "", // not supported yet
+            pod_name: "", // not supported
+            revision: GenericLogFilter.getDefaultOption("revision").value,
             service_name: GenericLogFilter.getDefaultOption("service_name").value,
         });
     };
@@ -246,6 +323,7 @@ const Logs: React.FC<Props> = ({
                                 <StyledLogs
                                     logs={logs}
                                     filters={filters}
+                                    appName={appName}
                                 />
                                 <LoadMoreButton
                                     active={selectedDate && logs.length !== 0}

+ 47 - 5
dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts

@@ -4,8 +4,15 @@ import { useEffect, useRef, useState } from "react";
 import api from "shared/api";
 import Anser from "anser";
 import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
-import { AgentLog, agentLogValidator, Direction, PorterLog, PaginationInfo, LogFilterName } from "../../expanded-app/logs/types";
-import { Service } from "../../new-app-flow/serviceTypes";
+import {
+  AgentLog,
+  agentLogValidator,
+  Direction,
+  PorterLog,
+  PaginationInfo,
+  LogFilterName,
+  GenericLogFilter
+} from "../../expanded-app/logs/types";
 
 const MAX_LOGS = 5000;
 const MAX_BUFFER_LOGS = 1000;
@@ -44,7 +51,8 @@ export const useLogs = (
   searchParam: string,
   notify: (message: string) => void,
   setLoading: (isLoading: boolean) => void,
-  // if setDate is set, results are not live
+  revisionIdToNumber: Record<string, number>,
+    // if setDate is set, results are not live
   setDate?: Date,
   timeRange?: {
     startTime?: Dayjs,
@@ -125,7 +133,7 @@ export const useLogs = (
         }
       }
 
-      return updatedLogs;
+      return filterLogs(updatedLogs);
     });
   };
 
@@ -185,7 +193,8 @@ export const useLogs = (
           }
         });
         const newLogsParsed = parseLogs(newLogs);
-        pushLogs(newLogsParsed);
+        const newLogsFiltered = filterLogs(newLogsParsed);
+        pushLogs(newLogsFiltered);
       },
       onclose: () => {
         console.log("Closed websocket:", websocketKey);
@@ -196,6 +205,26 @@ export const useLogs = (
     openWebsocket(websocketKey);
   };
 
+  const filterLogs = (logs: PorterLog[]) => {
+    return logs.filter(log => {
+      if (log.metadata == null) {
+        return true;
+      }
+
+      if (selectedFilterValues.output_stream !== GenericLogFilter.getDefaultOption("output_stream").value &&
+          log.metadata.output_stream !== selectedFilterValues.output_stream) {
+        return false;
+      }
+
+      if (selectedFilterValues.revision !== GenericLogFilter.getDefaultOption("revision").value &&
+          log.metadata.revision !== selectedFilterValues.revision) {
+        return false;
+      }
+
+      return true;
+    });
+  };
+
   const queryLogs = async (
     startDate: string,
     endDate: string,
@@ -239,6 +268,19 @@ export const useLogs = (
       if (direction === Direction.backward) {
         newLogs.reverse();
       }
+
+      newLogs.filter((log) => {
+        return log.metadata?.raw_labels?.porter_run_app_revision_id != null
+            && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != null
+            && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != 0
+      }).forEach((log) => {
+        if (log.metadata?.raw_labels?.porter_run_app_revision_id != null) {
+            const revisionNumber = revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id];
+            if (revisionNumber != null && revisionNumber != 0) {
+              log.metadata.revision = revisionNumber.toString();
+            }
+      }})
+
       return {
         logs: newLogs,
         previousCursor:

+ 1 - 1
go.mod

@@ -79,7 +79,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.0.98
+	github.com/porter-dev/api-contracts v0.0.99
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 2
go.sum

@@ -1489,8 +1489,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.0.98 h1:pchO+C7HKhpWZzR2RPDKKJnH3Rx8Cy/Q1v9aTfR9jzk=
-github.com/porter-dev/api-contracts v0.0.98/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.0.99 h1:7VltsUOtlTPlTApmcFyAhC29QxptgS87JNoeUk7VWGk=
+github.com/porter-dev/api-contracts v0.0.99/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 3 - 0
internal/porter_app/revisions.go

@@ -12,6 +12,8 @@ import (
 
 // Revision represents the data for a single revision
 type Revision struct {
+	// ID is the revision id
+	ID string `json:"id"`
 	// B64AppProto is the base64 encoded app proto definition
 	B64AppProto string `json:"b64_app_proto"`
 	// Status is the status of the revision
@@ -50,6 +52,7 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi
 	revision = Revision{
 		B64AppProto:    b64,
 		Status:         appRevision.Status,
+		ID:             appRevision.Id,
 		RevisionNumber: appRevision.RevisionNumber,
 		CreatedAt:      appRevision.CreatedAt.AsTime(),
 		UpdatedAt:      appRevision.UpdatedAt.AsTime(),