1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Multiple pods logging

This commit is contained in:
Pavel Ashevskiy 2021-03-09 15:19:44 +04:00
parent 33c405bdcf
commit b2ddcb527d
15 changed files with 7983 additions and 14 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"name": "lens-deployment-menu",
"version": "0.1.0",
"description": "Lens deployment menu",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "^26.6.3",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
}

View File

@ -0,0 +1,15 @@
import { LensRendererExtension } from "@k8slens/extensions";
import { DeploymentLogsMenu, DeploymentLogsMenuProps } from "./src/logs-menu";
import React from "react";
export default class DeploymentMenuRendererExtension extends LensRendererExtension {
kubeObjectMenuItems = [
{
kind: "Deployment",
apiVersions: ["apps/v1"],
components: {
MenuItem: (props: DeploymentLogsMenuProps) => <DeploymentLogsMenu {...props} />
}
}
];
}

View File

@ -0,0 +1,31 @@
import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions";
export interface DeploymentLogsMenuProps extends Component.KubeObjectMenuProps<K8sApi.Deployment> {
}
export class DeploymentLogsMenu extends React.Component<DeploymentLogsMenuProps> {
showLogs() {
Navigation.hideDetails();
const deployment = this.props.object;
const deploymentStore : K8sApi.DeploymentStore = K8sApi.apiManager.getStore(K8sApi.deploymentApi);
deploymentStore.getChildPods(deployment);
Component.logTabStore.createPodTab({
selectedPod: deploymentStore.getChildPods(deployment)[0],
selectedContainer: deploymentStore.getChildPods(deployment)[0].getContainers()[0],
});
}
render() {
const { object: deployment, toolbar } = this.props;
return (
<Component.MenuItem onClick={() => this.showLogs()}>
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
<span className="title">Logs</span>
</Component.MenuItem>
);
}
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -0,0 +1,35 @@
const path = require("path");
module.exports = [
{
entry: "./renderer.tsx",
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ ".tsx", ".ts", ".js" ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: "renderer.js",
path: path.resolve(__dirname, "dist"),
},
},
];

View File

@ -182,6 +182,7 @@
"extensions": [
"telemetry",
"pod-menu",
"deployment-menu",
"node-menu",
"metrics-cluster-feature",
"license-menu-item",

View File

@ -4,10 +4,10 @@ import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api";
export class PodsApi extends KubeApi<Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<any> {
const path = `${this.getUrl(params)}/log`;
return this.request.get(path, { query });
return this.request.getResponse(path, { query });
}
getMetrics(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {

View File

@ -72,6 +72,7 @@ export class DockTabStore<T> {
}
setData(tabId: TabId, data: T) {
console.log("SSSSSS", data)
this.data.set(tabId, data);
}

View File

@ -14,7 +14,7 @@ import { Button } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import { VirtualList } from "../virtual-list";
import { logStore } from "./log.store";
import { logsStore } from "./logs.store";
import { logTabStore } from "./log-tab.store";
interface Props {
@ -81,7 +81,7 @@ export class LogList extends React.Component<Props> {
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
if (!showTimestamps) {
return logStore.logsWithoutTimestamps;
return logsStore.logsWithoutTimestamps;
}
return this.props.logs;

View File

@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { Pod } from "../../api/endpoints";
import { Badge } from "../badge";
import { Icon } from "../icon";
import { Select, SelectOption } from "../select";
import { LogTabData, logTabStore } from "./log-tab.store";
import { podsStore } from "../+workloads-pods/pods.store";
@ -19,7 +20,7 @@ interface Props {
export const LogResourceSelector = observer((props: Props) => {
const { tabData, save, reload, tabId } = props;
const { selectedPod, selectedContainer, pods } = tabData;
const { selectedPod, selectedPods, selectedContainer, pods } = tabData;
const pod = new Pod(selectedPod);
const containers = pod.getContainers();
const initContainers = pod.getInitContainers();
@ -35,8 +36,15 @@ export const LogResourceSelector = observer((props: Props) => {
const onPodChange = (option: SelectOption) => {
const selectedPod = podsStore.getByName(option.value, pod.getNs());
save({ selectedPod });
const isAdded = props.tabData.selectedPods.findIndex((value) => value.getName() == option.value) > -1
let selectedPods = props.tabData.selectedPods;
if (isAdded) {
selectedPods = selectedPods.filter(value => value.getName() != option.value)
}
else {
selectedPods = selectedPods.concat(selectedPod)
}
save({ selectedPods: selectedPods });
logTabStore.renameTab(tabId);
};
@ -49,6 +57,18 @@ export const LogResourceSelector = observer((props: Props) => {
});
};
const formatPodOptionLabel = ({ value: pod }: SelectOption<String>) => {
console.log("sel",props.tabData.selectedPods)
const showLogs = props.tabData.selectedPods.findIndex((value) => value.metadata.name == pod) > -1
return (
<div className="flex gaps">
<span>{pod}</span>
{showLogs && <Icon small material="check" className="box right"/>}
</div>
);
};
const containerSelectOptions = [
{
label: `Containers`,
@ -69,7 +89,7 @@ export const LogResourceSelector = observer((props: Props) => {
useEffect(() => {
reload();
}, [selectedPod]);
}, [selectedPods]);
return (
<div className="LogResourceSelector flex gaps align-center">
@ -81,6 +101,7 @@ export const LogResourceSelector = observer((props: Props) => {
onChange={onPodChange}
autoConvertOptions={false}
className="pod-selector"
formatOptionLabel={formatPodOptionLabel}
/>
<span>Container</span>
<Select

View File

@ -10,6 +10,7 @@ import { dockStore, IDockTab, TabKind } from "./dock.store";
export interface LogTabData {
pods: Pod[];
selectedPod: Pod;
selectedPods?: Pod[];
selectedContainer: IPodContainer
showTimestamps?: boolean
previous?: boolean
@ -43,6 +44,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
this.createLogsTab(title, {
pods: pods.length ? pods : [selectedPod],
selectedPod,
selectedPods: [selectedPod],
selectedContainer
});
}

View File

@ -0,0 +1,67 @@
import { IPodLogsQuery, Pod, podsApi } from "../../api/endpoints";
interface LogStreamStatus {
done: boolean
lastTimestamp: number
}
export class LogsStreamManager {
logStreams: Map<string, LogStreamStatus> = new Map()
startLogStream = async (namespace: string, name: string, params: Partial<IPodLogsQuery>, onData: Function, onDone: Function, isDone: Function = this.isDone ) => {
if (!this.logStreams.get(namespace + "/" + name)){
this.logStreams.set(namespace + "/" + name, {done: false, lastTimestamp: 0})
podsApi.getLogs({ namespace:namespace, name: name }, {
...params,
follow: true,
timestamps: true, // Always setting timestampt to separate old logs from new ones
})
.then(response => response.body)
.then(rb => {
const reader = rb.getReader();
return new ReadableStream({
start(controller) {
// The following function handles each data chunk
function push() {
// "done" is a Boolean and value a "Uint8Array"
reader.read().then( ({done,value}:{done:Boolean, value:Uint8Array}) => {
// If there is no more data to read
if (done || isDone(namespace, name)) {
onDone()
controller.close();
return;
}
// Get the data and send it to the browser via the controller
controller.enqueue(value);
let data = new TextDecoder("utf-8").decode(value).split("\n")
data.pop()
for (let row of data) {
onData(name,row)
}
push();
});
}
push();
}
});
});
}
}
stopLogStream = (namespace: string, name: string) => {
if (this.logStreams.get(namespace + "/" + name)){
let status = this.logStreams.get(namespace + "/" + name)
status.done = true
this.logStreams.set(namespace + "/" + name, status)
}
}
isDone = (namespace: string, name: string) => {
console.log("is done",namespace + "/" + name, this.logStreams.get(namespace + "/" + name)?.done)
return this.logStreams.get(namespace + "/" + name)?.done ?? false}
}

View File

@ -0,0 +1,144 @@
import { autorun, computed, observable } from "mobx";
import { IPodLogsQuery, Pod, podsApi } from "../../api/endpoints";
import { autobind, interval } from "../../utils";
import { dockStore, TabId } from "./dock.store";
import { isLogsTab, logTabStore } from "./log-tab.store";
import {LogsStreamManager} from "./logs-stream-manager"
const logLinesToLoad = 500;
export interface LogRecord {
podName: string
timestamp: number
record: string
}
@autobind()
export class LogsStore {
logsStreamManager = new LogsStreamManager()
@observable podLogs = observable.map<TabId, LogRecord[]>();
/**
* Function prepares tailLines param for passing to API request
* Each time it increasing it's number, caused to fetch more logs.
* Also, it handles loading errors, rewriting whole logs with error
* messages
* @param tabId
*/
load = async (tabId: TabId) => {
try {
if (!this.podLogs.get(tabId)) {
await this.loadLogs(tabId, {
tailLines: this.lines + logLinesToLoad
},
(name: string,data: string) => this.podLogs.set(tabId, (this.podLogs.get(tabId) ?? []).concat({podName: name, timestamp: Date.parse(this.getTimestamps(data)?.[0]), record: this.removeTimestamps(data)})));
}
//this.refresher.start();
//this.podLogs.set(tabId, logs);
} catch ({error}) {
const message = [
{ podName: name, timestamp: 1, record: `Failed to load logs: ${error.message}`},
{ podName: name, timestamp: 1, record:`Reason: ${error.reason} (${error.code})`}];
//this.refresher.stop();
this.podLogs.set(tabId, message);
}
};
/**
* Main logs loading function adds necessary data to payload and makes
* an API request
* @param tabId
* @param params request parameters described in IPodLogsQuery interface
* @returns {Promise} A fetch request promise
*/
loadLogs = async (tabId: TabId, params: Partial<IPodLogsQuery>, callback : Function) => {
const data = logTabStore.getData(tabId);
let shutDownPods = new Set(this.logsStreamManager.logStreams.keys())
for (let selectedPod of data.selectedPods){
console.log("Load logs was called", selectedPod.getName())
const pod = new Pod(selectedPod);
const namespace = pod.getNs();
const name = pod.getName();
let status = this.logsStreamManager.logStreams.get(namespace + "/" + name)
shutDownPods.delete(namespace + "/" + name)
if (!status || status.done) {
this.logsStreamManager.startLogStream(namespace, name, params, callback, ()=>{console.log(name + " pod logs was stoped")})
console.log(name + " pod logs was started")
}
}
shutDownPods.forEach((v) => {
console.log(name + " pod logs should be stoped")
let status = this.logsStreamManager.logStreams.get(v)
status.done = true
this.logsStreamManager.logStreams.set(v, status)
})
console.log("this.logsStreamManager.logStreams", this.logsStreamManager.logStreams)
};
/**
* Converts logs into a string array
* @returns {number} Length of log lines
*/
@computed
get lines() {
const id = dockStore.selectedTabId;
const logs = this.podLogs.get(id);
return logs ? logs.length : 0;
}
/**
* Returns logs with timestamps for selected tab
*/
get logs(): string[] {
const id = dockStore.selectedTabId;
if (!this.podLogs.has(id)) return [];
return this.podLogs.get(id).map(item => "[" + item.podName+ "]:" + item.record);
}
get logsWithoutTimestamps(): string[] {
const id = dockStore.selectedTabId;
if (!this.podLogs.has(id)) return [];
return this.podLogs.get(id).map(item => "[" + item.podName+ "]:" + this.removeTimestamps(item.record));
}
/**
* It gets timestamps from all logs then returns last one + 1 second
* (this allows to avoid getting the last stamp in the selection)
* @param tabId
*/
getLastSinceTime(tabId: TabId) {
const logs = this.podLogs.get(tabId);
const timestamps = this.getTimestamps(logs[logs.length - 1].record);
const stamp = new Date(timestamps ? timestamps[0] : null);
stamp.setSeconds(stamp.getSeconds() + 1); // avoid duplicates from last second
return stamp.toISOString();
}
getTimestamps(logs: string) {
return logs.match(/^\d+\S+/gm);
}
removeTimestamps(logs: string) {
return logs.replace(/^\d+.*?\s/gm, "");
}
clearLogs(tabId: TabId) {
this.podLogs.delete(tabId);
}
}
export const logsStore = new LogsStore();

View File

@ -8,7 +8,7 @@ import { IDockTab } from "./dock.store";
import { InfoPanel } from "./info-panel";
import { LogResourceSelector } from "./log-resource-selector";
import { LogList } from "./log-list";
import { logStore } from "./log.store";
import { logsStore } from "./logs.store";
import { LogSearch } from "./log-search";
import { LogControls } from "./log-controls";
import { LogTabData, logTabStore } from "./log-tab.store";
@ -45,12 +45,13 @@ export class Logs extends React.Component<Props> {
load = async () => {
this.isLoading = true;
await logStore.load(this.tabId);
await logsStore.load(this.tabId);
this.isLoading = false;
};
reload = async () => {
logStore.clearLogs(this.tabId);
console.log("Reload is called")
logsStore.clearLogs(this.tabId);
await this.load();
};
@ -83,8 +84,8 @@ export class Logs extends React.Component<Props> {
}
renderResourceSelector() {
const logs = logStore.logs;
const searchLogs = this.tabData.showTimestamps ? logs : logStore.logsWithoutTimestamps;
const logs = logsStore.logs;
const searchLogs = this.tabData.showTimestamps ? logs : logsStore.logsWithoutTimestamps;
const controls = (
<div className="flex gaps">
<LogResourceSelector
@ -114,7 +115,7 @@ export class Logs extends React.Component<Props> {
}
render() {
const logs = logStore.logs;
const logs = logsStore.logs;
return (
<div className="PodLogs flex column">