From e50c4c39c1a55353f1a71e17995b7c46d9c3ffc8 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Thu, 13 Oct 2022 18:24:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=AE=B9=E5=99=A8?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/container.go | 17 ++ backend/app/dto/container.go | 12 + backend/app/service/container.go | 86 +++++- backend/app/service/image_test.go | 24 +- backend/router/ro_container.go | 1 + frontend/src/api/interface/container.ts | 10 + frontend/src/api/modules/container.ts | 3 + frontend/src/lang/modules/en.ts | 4 + frontend/src/lang/modules/zh.ts | 4 + frontend/src/utils/util.ts | 14 + .../src/views/container/container/index.vue | 21 +- .../container/container/monitor/index.vue | 288 ++++++++++++++++++ .../src/views/container/image/build/index.vue | 1 + frontend/src/views/host/terminal/index.vue | 1 + 14 files changed, 470 insertions(+), 16 deletions(-) create mode 100644 frontend/src/views/container/container/monitor/index.vue diff --git a/backend/app/api/v1/container.go b/backend/app/api/v1/container.go index 748eaec20..d2bea277e 100644 --- a/backend/app/api/v1/container.go +++ b/backend/app/api/v1/container.go @@ -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 { diff --git a/backend/app/dto/container.go b/backend/app/dto/container.go index dc8a1afaa..0ff054e64 100644 --- a/backend/app/dto/container.go +++ b/backend/app/dto/container.go @@ -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"` diff --git a/backend/app/service/container.go b/backend/app/service/container.go index 8029cd05d..4ede2c37d 100644 --- a/backend/app/service/container.go +++ b/backend/app/service/container.go @@ -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 +} diff --git a/backend/app/service/image_test.go b/backend/app/service/image_test.go index 33a515041..adecf7c76 100644 --- a/backend/app/service/image_test.go +++ b/backend/app/service/image_test.go @@ -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) { diff --git a/backend/router/ro_container.go b/backend/router/ro_container.go index 447246250..56e6c6afe 100644 --- a/backend/router/ro_container.go +++ b/backend/router/ro_container.go @@ -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) diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index 3d7b8948a..49d3ca613 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -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; diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index bc748bd7e..448255887 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -12,6 +12,9 @@ export const createContainer = (params: Container.ContainerCreate) => { export const getContainerLog = (params: Container.ContainerLogSearch) => { return http.post(`/containers/log`, params); }; +export const ContainerStats = (id: string) => { + return http.get(`/containers/stats/${id}`); +}; export const ContainerOperator = (params: Container.ContainerOperate) => { return http.post(`/containers/operate`, params); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 2a25600ee..a4c25b25d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -226,6 +226,10 @@ export default { scope: 'IP Scope', gateway: 'Gateway', + monitor: 'Monitor', + refreshTime: 'Refresh time', + cache: 'Cache', + volume: 'Volume', volumeName: 'Name', mountpoint: 'Mountpoint', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 97f825d4f..80c8dd16f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -190,6 +190,10 @@ export default { onFailure: '失败后重启(默认重启 5 次)', no: '不重启', + monitor: '监控', + refreshTime: '刷新间隔', + cache: '缓存', + image: '镜像', imagePull: '拉取镜像', imagePush: '推送镜像', diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 59583cd7f..c72385a2b 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -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; diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index 20ca28a8b..58fd09127 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -157,12 +157,14 @@ + diff --git a/frontend/src/views/container/image/build/index.vue b/frontend/src/views/container/image/build/index.vue index 8a24dc843..3d1a0fd47 100644 --- a/frontend/src/views/container/image/build/index.vue +++ b/frontend/src/views/container/image/build/index.vue @@ -131,6 +131,7 @@ const loadLogs = async (path: string) => { const onCloseLog = async () => { emit('search'); clearInterval(Number(timer)); + timer = null; }; const loadBuildDir = async (path: string) => { diff --git a/frontend/src/views/host/terminal/index.vue b/frontend/src/views/host/terminal/index.vue index ce2aab82d..b4266c1c6 100644 --- a/frontend/src/views/host/terminal/index.vue +++ b/frontend/src/views/host/terminal/index.vue @@ -406,6 +406,7 @@ onMounted(() => { }); onBeforeMount(() => { clearInterval(Number(timer)); + timer = null; });