feat: 完成容器监控功能

This commit is contained in:
ssongliu 2022-10-13 18:24:24 +08:00 committed by ssongliu
parent 53845e60b6
commit e50c4c39c1
14 changed files with 470 additions and 16 deletions

View File

@ -1,6 +1,8 @@
package v1
import (
"errors"
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
@ -64,6 +66,21 @@ func (b *BaseApi) ContainerOperation(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) ContainerStats(c *gin.Context) {
containerID, ok := c.Params.Get("id")
if !ok {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error container id in path"))
return
}
result, err := containerService.ContainerStats(containerID)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, result)
}
func (b *BaseApi) Inspect(c *gin.Context) {
var req dto.InspectReq
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -37,6 +37,18 @@ type ContainerCreate struct {
RestartPolicy string `json:"restartPolicy"`
}
type ContainterStats struct {
CPUPercent float64 `json:"cpuPercent"`
Memory float64 `json:"memory"`
Cache float64 `json:"cache"`
IORead float64 `json:"ioRead"`
IOWrite float64 `json:"ioWrite"`
NetworkRX float64 `json:"networkRX"`
NetworkTX float64 `json:"networkTX"`
ShotTime time.Time `json:"shotTime"`
}
type VolumeHelper struct {
SourceDir string `json:"sourceDir"`
ContainerDir string `json:"containerDir"`

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
"time"
@ -33,6 +34,7 @@ type IContainerService interface {
ContainerCreate(req dto.ContainerCreate) error
ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(param dto.ContainerLog) (string, error)
ContainerStats(id string) (*dto.ContainterStats, error)
Inspect(req dto.InspectReq) (string, error)
DeleteNetwork(req dto.BatchDelete) error
CreateNetwork(req dto.NetworkCreat) error
@ -159,27 +161,27 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error {
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
var err error
ctx := context.Background()
dc, err := docker.NewDockerClient()
client, err := docker.NewDockerClient()
if err != nil {
return err
}
switch req.Operation {
case constant.ContainerOpStart:
err = dc.ContainerStart(ctx, req.ContainerID, types.ContainerStartOptions{})
err = client.ContainerStart(ctx, req.ContainerID, types.ContainerStartOptions{})
case constant.ContainerOpStop:
err = dc.ContainerStop(ctx, req.ContainerID, nil)
err = client.ContainerStop(ctx, req.ContainerID, nil)
case constant.ContainerOpRestart:
err = dc.ContainerRestart(ctx, req.ContainerID, nil)
err = client.ContainerRestart(ctx, req.ContainerID, nil)
case constant.ContainerOpKill:
err = dc.ContainerKill(ctx, req.ContainerID, "SIGKILL")
err = client.ContainerKill(ctx, req.ContainerID, "SIGKILL")
case constant.ContainerOpPause:
err = dc.ContainerPause(ctx, req.ContainerID)
err = client.ContainerPause(ctx, req.ContainerID)
case constant.ContainerOpUnpause:
err = dc.ContainerUnpause(ctx, req.ContainerID)
err = client.ContainerUnpause(ctx, req.ContainerID)
case constant.ContainerOpRename:
err = dc.ContainerRename(ctx, req.ContainerID, req.NewName)
err = client.ContainerRename(ctx, req.ContainerID, req.NewName)
case constant.ContainerOpRemove:
err = dc.ContainerRemove(ctx, req.ContainerID, types.ContainerRemoveOptions{RemoveVolumes: true, RemoveLinks: true, Force: true})
err = client.ContainerRemove(ctx, req.ContainerID, types.ContainerRemoveOptions{RemoveVolumes: true, RemoveLinks: true, Force: true})
}
return err
}
@ -213,6 +215,40 @@ func (u *ContainerService) ContainerLogs(req dto.ContainerLog) (string, error) {
return buf.String(), nil
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainterStats, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
res, err := client.ContainerStats(context.TODO(), id, false)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var stats *types.StatsJSON
if err := json.Unmarshal(body, &stats); err != nil {
return nil, err
}
var data dto.ContainterStats
previousCPU := stats.PreCPUStats.CPUUsage.TotalUsage
previousSystem := stats.PreCPUStats.SystemUsage
data.CPUPercent = calculateCPUPercentUnix(previousCPU, previousSystem, stats)
data.IORead, data.IOWrite = calculateBlockIO(stats.BlkioStats)
data.Memory = float64(stats.MemoryStats.Usage) / 1024 / 1024
if cache, ok := stats.MemoryStats.Stats["cache"]; ok {
data.Cache = float64(cache) / 1024 / 1024
}
data.Memory = data.Memory - data.Cache
data.NetworkRX, data.NetworkTX = calculateNetwork(stats.Networks)
data.ShotTime = stats.Read
return &data, nil
}
func (u *ContainerService) PageNetwork(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
@ -404,3 +440,35 @@ func stringsToMap(list []string) map[string]string {
}
return lableMap
}
func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
var (
cpuPercent = 0.0
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU)
systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem)
)
if systemDelta > 0.0 && cpuDelta > 0.0 {
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0
}
return cpuPercent
}
func calculateBlockIO(blkio types.BlkioStats) (blkRead float64, blkWrite float64) {
for _, bioEntry := range blkio.IoServiceBytesRecursive {
switch strings.ToLower(bioEntry.Op) {
case "read":
blkRead = (blkRead + float64(bioEntry.Value)) / 1024 / 1024
case "write":
blkWrite = (blkWrite + float64(bioEntry.Value)) / 1024 / 1024
}
}
return
}
func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) {
var rx, tx float64
for _, v := range network {
rx += float64(v.RxBytes) / 1024
tx += float64(v.TxBytes) / 1024
}
return rx, tx
}

View File

@ -9,6 +9,7 @@ import (
"os"
"testing"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/docker/docker/api/types"
@ -95,10 +96,31 @@ func TestNetwork(t *testing.T) {
if err != nil {
fmt.Println(err)
}
_, err = client.NetworkCreate(context.TODO(), "test", types.NetworkCreate{})
res, err := client.ContainerStatsOneShot(context.TODO(), "30e4d3395b87")
if err != nil {
fmt.Println(err)
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
}
var state *types.StatsJSON
if err := json.Unmarshal(body, &state); err != nil {
fmt.Println(err)
}
fmt.Println(string(body))
var data dto.ContainterStats
previousCPU := state.PreCPUStats.CPUUsage.TotalUsage
previousSystem := state.PreCPUStats.SystemUsage
data.CPUPercent = calculateCPUPercentUnix(previousCPU, previousSystem, state)
data.IORead, data.IOWrite = calculateBlockIO(state.BlkioStats)
data.Memory = float64(state.MemoryStats.Usage)
data.NetworkRX, data.NetworkTX = calculateNetwork(state.Networks)
fmt.Println(data)
}
func TestContainer(t *testing.T) {

View File

@ -25,6 +25,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("", baseApi.ContainerCreate)
withRecordRouter.POST("operate", baseApi.ContainerOperation)
withRecordRouter.POST("/log", baseApi.ContainerLogs)
withRecordRouter.GET("/stats/:id", baseApi.ContainerStats)
baRouter.POST("/repo/search", baseApi.SearchRepo)
baRouter.PUT("/repo/:id", baseApi.UpdateRepo)

View File

@ -37,6 +37,16 @@ export namespace Container {
state: string;
runTime: string;
}
export interface ContainerStats {
cpuPercent: number;
memory: number;
cache: number;
ioRead: number;
ioWrite: number;
networkRX: number;
networkTX: number;
shotTime: Date;
}
export interface ContainerLogSearch {
containerID: string;
mode: string;

View File

@ -12,6 +12,9 @@ export const createContainer = (params: Container.ContainerCreate) => {
export const getContainerLog = (params: Container.ContainerLogSearch) => {
return http.post<string>(`/containers/log`, params);
};
export const ContainerStats = (id: string) => {
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
};
export const ContainerOperator = (params: Container.ContainerOperate) => {
return http.post(`/containers/operate`, params);

View File

@ -226,6 +226,10 @@ export default {
scope: 'IP Scope',
gateway: 'Gateway',
monitor: 'Monitor',
refreshTime: 'Refresh time',
cache: 'Cache',
volume: 'Volume',
volumeName: 'Name',
mountpoint: 'Mountpoint',

View File

@ -190,6 +190,10 @@ export default {
onFailure: '失败后重启默认重启 5 ',
no: '不重启',
monitor: '监控',
refreshTime: '刷新间隔',
cache: '缓存',
image: '镜像',
imagePull: '拉取镜像',
imagePush: '推送镜像',

View File

@ -49,6 +49,7 @@ export function dateFromat(row: number, col: number, dataStr: any) {
return `${String(y)}-${String(m)}-${String(d)} ${String(h)}:${String(minute)}:${String(second)}`;
}
// 20221013151302
export function dateFromatForName(dataStr: any) {
const date = new Date(dataStr);
const y = date.getFullYear();
@ -65,6 +66,7 @@ export function dateFromatForName(dataStr: any) {
return `${String(y)}${String(m)}${String(d)}${String(h)}${String(minute)}${String(second)}`;
}
// 10-13 \n 15:13
export function dateFromatWithoutYear(dataStr: any) {
const date = new Date(dataStr);
let m: string | number = date.getMonth() + 1;
@ -78,6 +80,18 @@ export function dateFromatWithoutYear(dataStr: any) {
return `${String(m)}-${String(d)}\n${String(h)}:${String(minute)}`;
}
// 20221013151302
export function dateFromatForSecond(dataStr: any) {
const date = new Date(dataStr);
let h: string | number = date.getHours();
h = h < 10 ? `0${String(h)}` : h;
let minute: string | number = date.getMinutes();
minute = minute < 10 ? `0${String(minute)}` : minute;
let second: string | number = date.getSeconds();
second = second < 10 ? `0${String(second)}` : second;
return `${String(h)}:${String(minute)}:${String(second)}`;
}
export function getRandomStr(e: number): string {
const t = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
const a = t.length;

View File

@ -157,12 +157,14 @@
</template>
</el-dialog>
<CreateDialog @search="search" ref="dialogCreateRef" />
<MonitorDialog ref="dialogMonitorRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import CreateDialog from '@/views/container/container/create/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
@ -239,15 +241,16 @@ const search = async () => {
});
};
const dialogCreateRef = ref<DialogExpose>();
interface DialogExpose {
acceptParams: () => void;
}
const onCreate = async () => {
const dialogCreateRef = ref();
const onCreate = () => {
dialogCreateRef.value!.acceptParams();
};
const dialogMonitorRef = ref();
const onMonitor = (containerID: string) => {
dialogMonitorRef.value!.acceptParams({ containerID: containerID });
};
const onInspect = async (id: string) => {
const res = await inspect({ id: id, type: 'container' });
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);
@ -364,6 +367,12 @@ const onOperate = async (operation: string) => {
};
const buttons = [
{
label: i18n.global.t('container.monitor'),
click: (row: Container.ContainerInfo) => {
onMonitor(row.containerID);
},
},
{
label: i18n.global.t('container.reName'),
click: (row: Container.ContainerInfo) => {

View File

@ -0,0 +1,288 @@
<template>
<el-dialog
v-model="monitorVisiable"
:destroy-on-close="true"
@close="onClose"
:close-on-click-modal="false"
width="70%"
>
<template #header>
<div class="card-header">
<span>{{ $t('container.monitor') }}</span>
</div>
</template>
<span>{{ $t('container.refreshTime') }}</span>
<el-select style="margin-left: 10px" v-model="timeInterval" @change="changeTimer">
<el-option label="1s" :value="1" />
<el-option label="3s" :value="3" />
<el-option label="5s" :value="5" />
<el-option label="10s" :value="10" />
<el-option label="30s" :value="30" />
<el-option label="60s" :value="60" />
</el-select>
<el-row :gutter="20" style="margin-top: 10px">
<el-col :span="12">
<el-card style="overflow: inherit">
<div id="cpuChart" style="width: 100%; height: 230px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card style="overflow: inherit">
<div id="memoryChart" style="width: 100%; height: 230px"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 10px">
<el-col :span="12">
<el-card style="overflow: inherit">
<div id="ioChart" style="width: 100%; height: 230px"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card style="overflow: inherit">
<div id="networkChart" style="width: 100%; height: 230px"></div>
</el-card>
</el-col>
</el-row>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ContainerStats } from '@/api/modules/container';
import { dateFromatForSecond } from '@/utils/util';
import * as echarts from 'echarts';
import i18n from '@/lang';
const monitorVisiable = ref(false);
const timeInterval = ref();
let timer: NodeJS.Timer | null = null;
let isInit = ref<boolean>(true);
interface DialogProps {
containerID: string;
}
const dialogData = ref<DialogProps>({
containerID: '',
});
function changeChartSize() {
echarts.getInstanceByDom(document.getElementById('cpuChart') as HTMLElement)?.resize();
echarts.getInstanceByDom(document.getElementById('memoryChart') as HTMLElement)?.resize();
echarts.getInstanceByDom(document.getElementById('ioChart') as HTMLElement)?.resize();
echarts.getInstanceByDom(document.getElementById('networkChart') as HTMLElement)?.resize();
}
const acceptParams = async (params: DialogProps): Promise<void> => {
monitorVisiable.value = true;
dialogData.value.containerID = params.containerID;
cpuDatas.value = [];
memDatas.value = [];
cacheDatas.value = [];
ioReadDatas.value = [];
ioWriteDatas.value = [];
netTxDatas.value = [];
netRxDatas.value = [];
timeDatas.value = [];
timeInterval.value = 5;
isInit.value = true;
loadData();
window.addEventListener('resize', changeChartSize);
timer = setInterval(async () => {
if (monitorVisiable.value) {
isInit.value = false;
loadData();
}
}, 1000 * timeInterval.value);
};
const cpuDatas = ref<Array<string>>([]);
const memDatas = ref<Array<string>>([]);
const cacheDatas = ref<Array<string>>([]);
const ioReadDatas = ref<Array<string>>([]);
const ioWriteDatas = ref<Array<string>>([]);
const netTxDatas = ref<Array<string>>([]);
const netRxDatas = ref<Array<string>>([]);
const timeDatas = ref<Array<string>>([]);
const changeTimer = () => {
clearInterval(Number(timer));
timer = setInterval(async () => {
if (monitorVisiable.value) {
loadData();
}
}, 1000 * timeInterval.value);
};
const loadData = async () => {
const res = await ContainerStats(dialogData.value.containerID);
cpuDatas.value.push(res.data.cpuPercent.toFixed(2));
if (cpuDatas.value.length > 20) {
cpuDatas.value.splice(0, 1);
}
memDatas.value.push(res.data.memory.toFixed(2));
if (memDatas.value.length > 20) {
memDatas.value.splice(0, 1);
}
cacheDatas.value.push(res.data.cache.toFixed(2));
if (cacheDatas.value.length > 20) {
cacheDatas.value.splice(0, 1);
}
ioReadDatas.value.push(res.data.ioRead.toFixed(2));
if (ioReadDatas.value.length > 20) {
ioReadDatas.value.splice(0, 1);
}
ioWriteDatas.value.push(res.data.ioWrite.toFixed(2));
if (ioWriteDatas.value.length > 20) {
ioWriteDatas.value.splice(0, 1);
}
netTxDatas.value.push(res.data.networkTX.toFixed(2));
if (netTxDatas.value.length > 20) {
netTxDatas.value.splice(0, 1);
}
netRxDatas.value.push(res.data.networkRX.toFixed(2));
if (netRxDatas.value.length > 20) {
netRxDatas.value.splice(0, 1);
}
timeDatas.value.push(dateFromatForSecond(res.data.shotTime));
if (timeDatas.value.length > 20) {
timeDatas.value.splice(0, 1);
}
let cpuYDatas = {
name: 'CPU',
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: cpuDatas.value,
showSymbol: false,
};
freshChart('cpuChart', ['CPU'], timeDatas.value, [cpuYDatas], 'CPU', '%');
let memoryYDatas = {
name: i18n.global.t('monitor.memory'),
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: memDatas.value,
showSymbol: false,
};
let cacheYDatas = {
name: i18n.global.t('container.cache'),
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: cacheDatas.value,
showSymbol: false,
};
freshChart(
'memoryChart',
[i18n.global.t('monitor.memory'), i18n.global.t('monitor.cache')],
timeDatas.value,
[memoryYDatas, cacheYDatas],
i18n.global.t('monitor.memory'),
' MB',
);
let ioReadYDatas = {
name: i18n.global.t('monitor.read'),
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: ioReadDatas.value,
showSymbol: false,
};
let ioWriteYDatas = {
name: i18n.global.t('monitor.write'),
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: ioWriteDatas.value,
showSymbol: false,
};
freshChart(
'ioChart',
[i18n.global.t('monitor.read'), i18n.global.t('monitor.write')],
timeDatas.value,
[ioReadYDatas, ioWriteYDatas],
i18n.global.t('monitor.disk') + ' IO',
'MB',
);
let netTxYDatas = {
name: i18n.global.t('monitor.up'),
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: netTxDatas.value,
showSymbol: false,
};
let netRxYDatas = {
name: i18n.global.t('monitor.down'),
type: 'line',
areaStyle: {
color: '#ebdee3',
},
data: netRxDatas.value,
showSymbol: false,
};
freshChart(
'networkChart',
[i18n.global.t('monitor.up'), i18n.global.t('monitor.down')],
timeDatas.value,
[netTxYDatas, netRxYDatas],
i18n.global.t('monitor.network'),
'KB/s',
);
};
function freshChart(chartName: string, legendDatas: any, xDatas: any, yDatas: any, yTitle: string, formatStr: string) {
if (isInit.value) {
echarts.init(document.getElementById(chartName) as HTMLElement);
}
let itemChart = echarts.getInstanceByDom(document.getElementById(chartName) as HTMLElement);
const option = {
title: [
{
left: 'center',
text: yTitle,
},
],
zlevel: 1,
z: 1,
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
let res = datas[0].name + '<br/>';
for (const item of datas) {
res += item.marker + ' ' + item.seriesName + '' + item.data + formatStr + '<br/>';
}
return res;
},
},
grid: { left: '7%', right: '7%', bottom: '20%' },
legend: {
data: legendDatas,
right: 10,
},
xAxis: { data: xDatas, boundaryGap: false },
yAxis: { name: '( ' + formatStr + ' )' },
series: yDatas,
};
itemChart?.setOption(option, true);
}
const onClose = async () => {
clearInterval(Number(timer));
timer = null;
window.removeEventListener('resize', changeChartSize);
};
defineExpose({
acceptParams,
});
</script>

View File

@ -131,6 +131,7 @@ const loadLogs = async (path: string) => {
const onCloseLog = async () => {
emit('search');
clearInterval(Number(timer));
timer = null;
};
const loadBuildDir = async (path: string) => {

View File

@ -406,6 +406,7 @@ onMounted(() => {
});
onBeforeMount(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped>