فهرست منبع

Merge pull request #1526 from porter-dev/nico/por-279-extend-log-support-on-expanded-chart

[POR-279] Extend log support on expanded chart
abelanger5 4 سال پیش
والد
کامیت
6cefb49843

+ 7 - 1
api/server/handlers/namespace/stream_pod_logs.go

@@ -34,6 +34,12 @@ func NewStreamPodLogsHandler(
 }
 
 func (c *StreamPodLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	request := &types.GetPodLogsRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
 	safeRW := r.Context().Value(types.RequestCtxWebsocketKey).(*websocket.WebsocketSafeReadWriter)
 	namespace := r.Context().Value(types.NamespaceScope).(string)
 	name, _ := requestutils.GetURLParamString(r, types.URLParamPodName)
@@ -47,7 +53,7 @@ func (c *StreamPodLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	err = agent.GetPodLogs(namespace, name, safeRW)
+	err = agent.GetPodLogs(namespace, name, request.Previous, request.Container, safeRW)
 
 	if targetErr := kubernetes.IsNotFoundError; errors.Is(err, targetErr) {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(

+ 5 - 0
api/types/namespace.go

@@ -111,3 +111,8 @@ type RenameConfigMapResponse struct {
 type DeleteConfigMapRequest struct {
 	Name string `schema:"name,required"`
 }
+
+type GetPodLogsRequest struct {
+	Container string `schema:"container_name"`
+	Previous  bool   `schema:"previous"`
+}

+ 122 - 26
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -1,8 +1,9 @@
-import React, { Component } from "react";
+import React, { Component, useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import * as Anser from "anser";
 import api from "shared/api";
+import { useWebsockets } from "shared/hooks/useWebsockets";
 
 const MAX_LOGS = 1000;
 
@@ -18,6 +19,7 @@ type StateType = {
   ws: any;
   scroll: boolean;
   currentTab: string;
+  getPreviousLogs: boolean;
 };
 
 export default class Logs extends Component<PropsType, StateType> {
@@ -29,11 +31,43 @@ export default class Logs extends Component<PropsType, StateType> {
     ws: null as any,
     scroll: true,
     currentTab: "Application",
+    getPreviousLogs: false,
   };
 
   ws = null as any;
   parentRef = React.createRef<HTMLDivElement>();
 
+  getPodStatus = (status: any) => {
+    if (
+      status?.phase === "Pending" &&
+      status?.containerStatuses !== undefined
+    ) {
+      return status.containerStatuses[0].state.waiting.reason;
+    } else if (status?.phase === "Pending") {
+      return "Pending";
+    }
+
+    if (status?.phase === "Failed") {
+      return "failed";
+    }
+
+    if (status?.phase === "Running") {
+      let collatedStatus = "running";
+
+      status?.containerStatuses?.forEach((s: any) => {
+        if (s.state?.waiting) {
+          collatedStatus =
+            s.state?.waiting.reason === "CrashLoopBackOff"
+              ? "failed"
+              : "waiting";
+        } else if (s.state?.terminated) {
+          collatedStatus = "failed";
+        }
+      });
+      return collatedStatus;
+    }
+  };
+
   scrollToBottom = (smooth: boolean) => {
     if (smooth) {
       this.parentRef.current.lastElementChild.scrollIntoView({
@@ -69,6 +103,27 @@ export default class Logs extends Component<PropsType, StateType> {
       );
     }
 
+    if (
+      this.getPodStatus(selectedPod.status) === "failed" &&
+      this.state.logs.length === 0
+    ) {
+      return (
+        <Message>
+          No logs to display from this pod.
+          <Highlight
+            onClick={() => {
+              this.setState({ getPreviousLogs: true }, () => {
+                this.refreshLogs();
+              });
+            }}
+          >
+            <i className="material-icons">autorenew</i>
+            Get logs from crashed pod
+          </Highlight>
+        </Message>
+      );
+    }
+
     if (this.state.logs.length == 0) {
       return (
         <Message>
@@ -106,10 +161,16 @@ export default class Logs extends Component<PropsType, StateType> {
     let { selectedPod } = this.props;
     if (!selectedPod?.metadata?.name) return;
     let protocol = window.location.protocol == "https:" ? "wss" : "ws";
-
-    this.ws = new WebSocket(
-      `${protocol}://${window.location.host}/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs`
-    );
+    const currentTab = this.state.currentTab;
+    if (currentTab === "Application") {
+      this.ws = new WebSocket(
+        `${protocol}://${window.location.host}/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?previous=${this.state.getPreviousLogs}`
+      );
+    } else {
+      this.ws = new WebSocket(
+        `${protocol}://${window.location.host}/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?container_name=${currentTab}&previous=${this.state.getPreviousLogs}`
+      );
+    }
 
     this.ws.onopen = () => {};
 
@@ -148,7 +209,11 @@ export default class Logs extends Component<PropsType, StateType> {
 
   refreshLogs = () => {
     let { selectedPod } = this.props;
-    if (this.ws && this.state.currentTab == "Application") {
+    if (
+      this.ws &&
+      typeof this.state.currentTab === "string" &&
+      this.state.currentTab != "System"
+    ) {
       this.ws.close();
       this.ws = null;
       this.setState({ logs: [] });
@@ -166,13 +231,14 @@ export default class Logs extends Component<PropsType, StateType> {
 
       this.setState({ logs: [] });
 
-      if (this.state.currentTab == "Application") {
-        this.setupWebsocket();
-        this.scrollToBottom(false);
+      if (this.state.currentTab == "System") {
+        this.retrieveEvents(selectedPod);
         return;
       }
 
-      this.retrieveEvents(selectedPod);
+      this.setState({ getPreviousLogs: false });
+      this.setupWebsocket();
+      this.scrollToBottom(false);
     }
   };
 
@@ -211,6 +277,15 @@ export default class Logs extends Component<PropsType, StateType> {
   componentDidMount() {
     let { selectedPod } = this.props;
 
+    if (selectedPod?.spec?.containers?.length > 1) {
+      const firstContainer = selectedPod?.spec?.containers[0];
+      this.setState({ currentTab: firstContainer?.name }, () => {
+        this.setupWebsocket();
+        this.scrollToBottom(false);
+      });
+      return;
+    }
+
     if (this.state.currentTab == "Application") {
       this.setupWebsocket();
       this.scrollToBottom(false);
@@ -226,20 +301,48 @@ export default class Logs extends Component<PropsType, StateType> {
     }
   }
 
-  render() {
-    if (this.props.rawText) {
+  renderContainerTabs = () => {
+    const containers = this.props.selectedPod?.spec?.containers;
+
+    if (!Array.isArray(containers) || containers?.length <= 1) {
       return (
-        <LogStreamAlt>
-          <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
-          <LogTabs>
+        <Tab
+          onClick={() => {
+            this.setState({ currentTab: "Application" });
+          }}
+          clicked={this.state.currentTab == "Application"}
+        >
+          Application
+        </Tab>
+      );
+    }
+
+    return (
+      <>
+        {containers.map((container: any) => {
+          return (
             <Tab
+              key={container.name}
               onClick={() => {
-                this.setState({ currentTab: "Application" });
+                this.setState({ currentTab: container.name });
               }}
-              clicked={this.state.currentTab == "Application"}
+              clicked={this.state.currentTab == container.name}
             >
-              Application
+              {container.name}
             </Tab>
+          );
+        })}
+      </>
+    );
+  };
+
+  render() {
+    if (this.props.rawText) {
+      return (
+        <LogStreamAlt>
+          <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
+          <LogTabs>
+            {this.renderContainerTabs()}
             <Tab
               onClick={() => {
                 this.setState({ currentTab: "System" });
@@ -283,14 +386,7 @@ export default class Logs extends Component<PropsType, StateType> {
       <LogStream>
         <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
         <LogTabs>
-          <Tab
-            onClick={() => {
-              this.setState({ currentTab: "Application" });
-            }}
-            clicked={this.state.currentTab == "Application"}
-          >
-            Application
-          </Tab>
+          {this.renderContainerTabs()}
           <Tab
             onClick={() => {
               this.setState({ currentTab: "System" });

+ 6 - 1
internal/kubernetes/agent.go

@@ -554,7 +554,7 @@ func (a *Agent) DeletePod(namespace string, name string) error {
 }
 
 // GetPodLogs streams real-time logs from a given pod.
-func (a *Agent) GetPodLogs(namespace string, name string, rw *websocket.WebsocketSafeReadWriter) error {
+func (a *Agent) GetPodLogs(namespace string, name string, showPreviousLogs bool, selectedContainer string, rw *websocket.WebsocketSafeReadWriter) error {
 	// get the pod to read in the list of contains
 	pod, err := a.Clientset.CoreV1().Pods(namespace).Get(
 		context.Background(),
@@ -580,6 +580,10 @@ func (a *Agent) GetPodLogs(namespace string, name string, rw *websocket.Websocke
 
 	container := pod.Spec.Containers[0].Name
 
+	if len(selectedContainer) > 0 {
+		container = selectedContainer
+	}
+
 	tails := int64(400)
 
 	// follow logs
@@ -587,6 +591,7 @@ func (a *Agent) GetPodLogs(namespace string, name string, rw *websocket.Websocke
 		Follow:    true,
 		TailLines: &tails,
 		Container: container,
+		Previous:  showPreviousLogs,
 	}
 
 	req := a.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)