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:
parent
33c405bdcf
commit
b2ddcb527d
7600
extensions/deployment-menu/package-lock.json
generated
Normal file
7600
extensions/deployment-menu/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
extensions/deployment-menu/package.json
Normal file
25
extensions/deployment-menu/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
15
extensions/deployment-menu/renderer.tsx
Normal file
15
extensions/deployment-menu/renderer.tsx
Normal 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} />
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
31
extensions/deployment-menu/src/logs-menu.tsx
Normal file
31
extensions/deployment-menu/src/logs-menu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
26
extensions/deployment-menu/tsconfig.json
Normal file
26
extensions/deployment-menu/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
35
extensions/deployment-menu/webpack.config.js
Normal file
35
extensions/deployment-menu/webpack.config.js
Normal 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"),
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -182,6 +182,7 @@
|
||||
"extensions": [
|
||||
"telemetry",
|
||||
"pod-menu",
|
||||
"deployment-menu",
|
||||
"node-menu",
|
||||
"metrics-cluster-feature",
|
||||
"license-menu-item",
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -72,6 +72,7 @@ export class DockTabStore<T> {
|
||||
}
|
||||
|
||||
setData(tabId: TabId, data: T) {
|
||||
console.log("SSSSSS", data)
|
||||
this.data.set(tabId, data);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
67
src/renderer/components/dock/logs-stream-manager.ts
Normal file
67
src/renderer/components/dock/logs-stream-manager.ts
Normal 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}
|
||||
}
|
||||
144
src/renderer/components/dock/logs.store.ts
Normal file
144
src/renderer/components/dock/logs.store.ts
Normal 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();
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user