fix: полный production-фикс fix40 (layout, sidebar, journald, логирование)
This commit is contained in:
+13
-20
@@ -7,6 +7,7 @@ import (
|
|||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/model/dto"
|
"hy2xs-admin/model/dto"
|
||||||
"hy2xs-admin/model/vo"
|
"hy2xs-admin/model/vo"
|
||||||
|
"hy2xs-admin/service"
|
||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -55,34 +56,17 @@ func LogHysteria2(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
exists := util.Exists(constant.Hysteria2LogPath)
|
|
||||||
logHysteria2Vos := make([]vo.LogHysteria2Vo, 0)
|
logHysteria2Vos := make([]vo.LogHysteria2Vo, 0)
|
||||||
if !exists {
|
|
||||||
vo.Success(logHysteria2Vos, c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
numLine := 0
|
numLine := 0
|
||||||
if logSystemDto.NumLine != nil && *logSystemDto.NumLine > 0 {
|
if logSystemDto.NumLine != nil && *logSystemDto.NumLine > 0 {
|
||||||
numLine = *logSystemDto.NumLine
|
numLine = *logSystemDto.NumLine
|
||||||
}
|
}
|
||||||
logLines, total, err := util.ReadLinesFromBottom(constant.Hysteria2LogPath, numLine)
|
logHysteria2Vos, total, err := service.ReadHysteriaJournalLogs(numLine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail("Unable to read log file", c)
|
vo.Fail("Unable to read hysteria journal logs", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range logLines {
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logHysteria2Vo := vo.LogHysteria2Vo{}
|
|
||||||
err := json.Unmarshal([]byte(line), &logHysteria2Vo)
|
|
||||||
if err != nil {
|
|
||||||
vo.Fail("Unable to unmarshal log data", c)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logHysteria2Vos = append(logHysteria2Vos, logHysteria2Vo)
|
|
||||||
}
|
|
||||||
vo.Success(vo.LogSystemPage[vo.LogHysteria2Vo]{
|
vo.Success(vo.LogSystemPage[vo.LogHysteria2Vo]{
|
||||||
LogSystemVos: logHysteria2Vos,
|
LogSystemVos: logHysteria2Vos,
|
||||||
Total: int64(total),
|
Total: int64(total),
|
||||||
@@ -102,7 +86,16 @@ func ExportLog(c *gin.Context) {
|
|||||||
filePath = constant.SystemLogPath
|
filePath = constant.SystemLogPath
|
||||||
} else if *logExportDto.Option == 1 {
|
} else if *logExportDto.Option == 1 {
|
||||||
fileName = fmt.Sprintf("hysteria2-%s.log", time.Now().Format("20060102150405"))
|
fileName = fmt.Sprintf("hysteria2-%s.log", time.Now().Format("20060102150405"))
|
||||||
filePath = constant.Hysteria2LogPath
|
output, exportErr := service.ExportHysteriaJournalLogs(5000)
|
||||||
|
if exportErr != nil {
|
||||||
|
vo.Fail("failed to export hysteria journal logs", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
c.Header("Content-Transfer-Encoding", "binary")
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
|
||||||
|
c.Data(200, "text/plain; charset=utf-8", []byte(output))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !util.Exists(filePath) {
|
if !util.Exists(filePath) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const appStore = useAppStore();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-config-provider :locale="appStore.locale" :size="appStore.size">
|
<el-config-provider :locale="appStore.locale">
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-config-provider>
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 211 B |
@@ -1,40 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useAppStore } from "@/store/modules/app";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const sizeOptions = ref([
|
|
||||||
{ label: t("common.sizeDefault"), value: "default" },
|
|
||||||
{ label: t("common.sizeLarge"), value: "large" },
|
|
||||||
{ label: t("common.sizeSmall"), value: "small" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
function handleSizeChange(size: string) {
|
|
||||||
appStore.changeSize(size);
|
|
||||||
ElMessage.success(t("common.sizeChanged"));
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-dropdown trigger="click" @command="handleSizeChange">
|
|
||||||
<div>
|
|
||||||
<svg-icon icon-class="size" />
|
|
||||||
</div>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item
|
|
||||||
v-for="item of sizeOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:disabled="appStore.size == item.value"
|
|
||||||
:command="item.value"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
@@ -112,10 +112,6 @@ export default {
|
|||||||
resetTrafficConfirm: "Are you sure to reset peer traffic?",
|
resetTrafficConfirm: "Are you sure to reset peer traffic?",
|
||||||
invalid: "Invalid value",
|
invalid: "Invalid value",
|
||||||
switchLanguageSuccess: "Language switched successfully",
|
switchLanguageSuccess: "Language switched successfully",
|
||||||
sizeChanged: "Interface size changed",
|
|
||||||
sizeDefault: "Default",
|
|
||||||
sizeLarge: "Large",
|
|
||||||
sizeSmall: "Small",
|
|
||||||
logoutConfirm: "Are you sure you want to log out?",
|
logoutConfirm: "Are you sure you want to log out?",
|
||||||
sessionExpired: "Current session has expired, please log in again",
|
sessionExpired: "Current session has expired, please log in again",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -108,10 +108,6 @@ export default {
|
|||||||
resetTrafficConfirm: "Сбросить трафик для пира?",
|
resetTrafficConfirm: "Сбросить трафик для пира?",
|
||||||
invalid: "Некорректное значение",
|
invalid: "Некорректное значение",
|
||||||
switchLanguageSuccess: "Язык переключён",
|
switchLanguageSuccess: "Язык переключён",
|
||||||
sizeChanged: "Размер интерфейса изменён",
|
|
||||||
sizeDefault: "Обычный",
|
|
||||||
sizeLarge: "Крупный",
|
|
||||||
sizeSmall: "Компактный",
|
|
||||||
logoutConfirm: "Выйти из системы?",
|
logoutConfirm: "Выйти из системы?",
|
||||||
sessionExpired: "Текущая сессия истекла, войдите снова",
|
sessionExpired: "Текущая сессия истекла, войдите снова",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function logout() {
|
|||||||
<!-- Верхняя навигационная панель -->
|
<!-- Верхняя навигационная панель -->
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<!-- Хлебные крошки слева -->
|
<!-- Хлебные крошки слева -->
|
||||||
<div class="flex">
|
<div class="navbar-left">
|
||||||
<hamburger
|
<hamburger
|
||||||
:is-active="appStore.sidebar.opened"
|
:is-active="appStore.sidebar.opened"
|
||||||
@toggleClick="toggleSideBar"
|
@toggleClick="toggleSideBar"
|
||||||
@@ -62,7 +62,7 @@ function logout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Настройки навигации справа -->
|
<!-- Настройки навигации справа -->
|
||||||
<div class="flex">
|
<div class="navbar-actions">
|
||||||
<!-- Настройки навигации, скрытые на узком экране -->
|
<!-- Настройки навигации, скрытые на узком экране -->
|
||||||
<div class="setting-container" v-if="device !== 'mobile'">
|
<div class="setting-container" v-if="device !== 'mobile'">
|
||||||
<!-- Полноэкранный режим -->
|
<!-- Полноэкранный режим -->
|
||||||
@@ -71,30 +71,28 @@ function logout() {
|
|||||||
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
:icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Размер интерфейса -->
|
|
||||||
<el-tooltip content="Размер интерфейса" effect="dark" placement="bottom">
|
|
||||||
<size-select class="setting-item" />
|
|
||||||
</el-tooltip>
|
|
||||||
<!-- Выбор языка -->
|
<!-- Выбор языка -->
|
||||||
<lang-select class="setting-item" />
|
<lang-select class="setting-item" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Аватар пользователя -->
|
<!-- Аватар пользователя -->
|
||||||
<el-dropdown trigger="click">
|
<div class="avatar-dropdown">
|
||||||
<div class="avatar-container">
|
<el-dropdown trigger="click">
|
||||||
<el-avatar :size="32" class="avatar-icon">
|
<div class="avatar-container">
|
||||||
<i-ep-user-filled />
|
<el-avatar :size="32" class="avatar-icon">
|
||||||
</el-avatar>
|
<i-ep-user-filled />
|
||||||
<i-ep-caret-bottom class="w-3 h-3" />
|
</el-avatar>
|
||||||
</div>
|
<i-ep-caret-bottom class="w-3 h-3" />
|
||||||
<template #dropdown>
|
</div>
|
||||||
<el-dropdown-menu>
|
<template #dropdown>
|
||||||
<el-dropdown-item @click="logout">
|
<el-dropdown-menu>
|
||||||
{{ $t("navbar.logout") }}
|
<el-dropdown-item @click="logout">
|
||||||
</el-dropdown-item>
|
{{ $t("navbar.logout") }}
|
||||||
</el-dropdown-menu>
|
</el-dropdown-item>
|
||||||
</template>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -108,17 +106,24 @@ function logout() {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 0 1px #0003;
|
box-shadow: 0 0 1px #0003;
|
||||||
|
|
||||||
.setting-container {
|
.navbar-left,
|
||||||
|
.navbar-actions,
|
||||||
|
.setting-container,
|
||||||
|
.avatar-dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-container {
|
||||||
.setting-item {
|
.setting-item {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: normal;
|
||||||
color: #5a5e66;
|
color: #5a5e66;
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -132,8 +137,8 @@ function logout() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
height: 36px;
|
height: 50px;
|
||||||
margin: 0 8px;
|
margin: 0;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function handleOutsideClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hideSidebar .fixed-header {
|
.hideSidebar .fixed-header {
|
||||||
width: calc(100% - 54px);
|
width: calc(100% - #{$sideBarCollapsedWidth});
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile .fixed-header {
|
.mobile .fixed-header {
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ interface DefaultSettings {
|
|||||||
*/
|
*/
|
||||||
theme: string;
|
theme: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Размер интерфейса
|
|
||||||
*/
|
|
||||||
size: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Язык
|
* Язык
|
||||||
*/
|
*/
|
||||||
@@ -60,7 +55,6 @@ const defaultSettings: DefaultSettings = {
|
|||||||
* light: Светлая тема
|
* light: Светлая тема
|
||||||
*/
|
*/
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
size: "default", // default |large |small
|
|
||||||
language: "ru", // ru | en
|
language: "ru", // ru | en
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import ru from "element-plus/es/locale/lang/ru";
|
|||||||
export const useAppStore = defineStore("app", () => {
|
export const useAppStore = defineStore("app", () => {
|
||||||
// state
|
// state
|
||||||
const device = useStorage("device", "desktop");
|
const device = useStorage("device", "desktop");
|
||||||
const size = useStorage<any>("size", defaultSettings.size);
|
|
||||||
const language = useStorage("language", defaultSettings.language);
|
const language = useStorage("language", defaultSettings.language);
|
||||||
|
|
||||||
const sidebarStatus = useStorage("sidebarStatus", "closed");
|
const sidebarStatus = useStorage("sidebarStatus", "closed");
|
||||||
@@ -52,9 +51,6 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
device.value = val;
|
device.value = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSize(val: string) {
|
|
||||||
size.value = val;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Переключение языка
|
* Переключение языка
|
||||||
*
|
*
|
||||||
@@ -69,9 +65,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
sidebar,
|
sidebar,
|
||||||
language,
|
language,
|
||||||
locale,
|
locale,
|
||||||
size,
|
|
||||||
toggleDevice,
|
toggleDevice,
|
||||||
changeSize,
|
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
closeSideBar,
|
closeSideBar,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
|
|
||||||
.hideSidebar {
|
.hideSidebar {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
width: 54px !important;
|
width: $sideBarCollapsedWidth !important;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
@@ -117,7 +117,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
margin-left: 54px;
|
margin-left: $sideBarCollapsedWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container .el-menu--collapse {
|
||||||
|
width: $sideBarCollapsedWidth !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container .el-menu--collapse > .el-menu-item,
|
||||||
|
.sidebar-container .el-menu--collapse > .el-sub-menu > .el-sub-menu__title {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container .el-menu--collapse .svg-icon {
|
||||||
|
display: block;
|
||||||
|
flex: 0 0 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-sub-menu {
|
.el-sub-menu {
|
||||||
@@ -146,7 +165,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-menu--collapse .el-menu .el-sub-menu {
|
.el-menu--collapse .el-menu .el-sub-menu {
|
||||||
min-width: $sideBarWidth !important;
|
min-width: $sideBarCollapsedWidth !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mobile responsive
|
// mobile responsive
|
||||||
@@ -218,4 +237,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ $subMenuActiveText: var(--subMenuActiveText);
|
|||||||
$subMenuHover: var(--subMenuHover);
|
$subMenuHover: var(--subMenuHover);
|
||||||
|
|
||||||
$sideBarWidth: 210px;
|
$sideBarWidth: 210px;
|
||||||
|
$sideBarCollapsedWidth: 54px;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
-1
@@ -60,7 +60,6 @@ declare module '@vue/runtime-core' {
|
|||||||
RightPanel: typeof import('./../components/RightPanel/index.vue')['default']
|
RightPanel: typeof import('./../components/RightPanel/index.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SizeSelect: typeof import('./../components/SizeSelect/index.vue')['default']
|
|
||||||
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
|
SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
|
||||||
UnitSelect: typeof import('./../components/UnitSelect/index.vue')['default']
|
UnitSelect: typeof import('./../components/UnitSelect/index.vue')['default']
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-14
@@ -18,26 +18,32 @@ func InitLog() {
|
|||||||
LocalTime: true,
|
LocalTime: true,
|
||||||
})
|
})
|
||||||
logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})
|
logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})
|
||||||
logrus.SetLevel(logrus.WarnLevel)
|
logrus.SetLevel(logrus.InfoLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogHandler() gin.HandlerFunc {
|
func LogHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
endTime := time.Now()
|
|
||||||
statusCode := c.Writer.Status()
|
|
||||||
latencyTime := endTime.Sub(startTime)
|
|
||||||
clientIP := c.ClientIP()
|
|
||||||
reqMethod := c.Request.Method
|
|
||||||
reqUri := c.Request.RequestURI
|
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"statusCode": statusCode,
|
|
||||||
"latencyTime": latencyTime,
|
|
||||||
"clientIP": clientIP,
|
|
||||||
"reqMethod": reqMethod,
|
|
||||||
"reqUri": reqUri,
|
|
||||||
}).Info()
|
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
|
statusCode := c.Writer.Status()
|
||||||
|
latencyTime := time.Since(startTime)
|
||||||
|
|
||||||
|
entry := logrus.WithFields(logrus.Fields{
|
||||||
|
"statusCode": statusCode,
|
||||||
|
"latencyTime": latencyTime.Milliseconds(),
|
||||||
|
"clientIP": c.ClientIP(),
|
||||||
|
"reqMethod": c.Request.Method,
|
||||||
|
"reqUri": c.Request.RequestURI,
|
||||||
|
})
|
||||||
|
|
||||||
|
if statusCode >= 500 {
|
||||||
|
entry.Error()
|
||||||
|
} else if statusCode >= 400 {
|
||||||
|
entry.Warn()
|
||||||
|
} else {
|
||||||
|
entry.Info()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ var (
|
|||||||
Hysteria2ConfigPath = "/etc/hysteria/config.yaml"
|
Hysteria2ConfigPath = "/etc/hysteria/config.yaml"
|
||||||
Hysteria2BinPath = "/usr/local/bin/hysteria"
|
Hysteria2BinPath = "/usr/local/bin/hysteria"
|
||||||
|
|
||||||
SystemLogPath = filepath.Join(LogDir, "hy2xs-admin.log")
|
SystemLogPath = filepath.Join(LogDir, "hy2xs-admin.log")
|
||||||
Hysteria2LogPath = filepath.Join(LogDir, "hysteria2.log")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"hy2xs-admin/model/vo"
|
||||||
|
"hy2xs-admin/util"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type journalctlLogLine struct {
|
||||||
|
Message string `json:"MESSAGE"`
|
||||||
|
Priority string `json:"PRIORITY"`
|
||||||
|
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadHysteriaJournalLogs(numLine int) ([]vo.LogHysteria2Vo, int, error) {
|
||||||
|
lines := 100
|
||||||
|
if numLine > 0 {
|
||||||
|
lines = numLine
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := util.Exec(fmt.Sprintf("journalctl --no-pager -u hysteria-server.service -n %d -o json", lines))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]vo.LogHysteria2Vo, 0, lines)
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
item := journalctlLogLine{}
|
||||||
|
if err = json.Unmarshal([]byte(line), &item); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, parseHysteriaJournalRecord(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanErr := scanner.Err(); scanErr != nil {
|
||||||
|
return nil, 0, scanErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, len(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExportHysteriaJournalLogs(numLine int) (string, error) {
|
||||||
|
lines := 5000
|
||||||
|
if numLine > 0 {
|
||||||
|
lines = numLine
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.Exec(fmt.Sprintf("journalctl --no-pager -u hysteria-server.service -n %d -o short-iso", lines))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHysteriaJournalRecord(item journalctlLogLine) vo.LogHysteria2Vo {
|
||||||
|
fallbackTime := convertJournalTimestamp(item.RealtimeTimestamp)
|
||||||
|
fallbackLevel := mapJournalPriorityToLevel(item.Priority)
|
||||||
|
|
||||||
|
if strings.TrimSpace(item.Message) == "" {
|
||||||
|
return vo.LogHysteria2Vo{
|
||||||
|
Level: fallbackLevel,
|
||||||
|
Msg: "",
|
||||||
|
Time: fallbackTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := vo.LogHysteria2Vo{}
|
||||||
|
if err := json.Unmarshal([]byte(item.Message), &parsed); err == nil {
|
||||||
|
if parsed.Level == "" {
|
||||||
|
parsed.Level = fallbackLevel
|
||||||
|
}
|
||||||
|
if parsed.Time == "" {
|
||||||
|
parsed.Time = fallbackTime
|
||||||
|
}
|
||||||
|
if parsed.Msg == "" {
|
||||||
|
parsed.Msg = item.Message
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return vo.LogHysteria2Vo{
|
||||||
|
Level: fallbackLevel,
|
||||||
|
Msg: item.Message,
|
||||||
|
Time: fallbackTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertJournalTimestamp(raw string) string {
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
us, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.UnixMicro(us)
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapJournalPriorityToLevel(priority string) string {
|
||||||
|
switch strings.TrimSpace(priority) {
|
||||||
|
case "0", "1", "2", "3":
|
||||||
|
return "error"
|
||||||
|
case "4":
|
||||||
|
return "warn"
|
||||||
|
default:
|
||||||
|
return "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -73,6 +73,10 @@ journalctl -u hysteria-server -n 100 --no-pager
|
|||||||
journalctl -u hy2xs-admin -n 100 --no-pager
|
journalctl -u hy2xs-admin -n 100 --no-pager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Источник логов в UI:
|
||||||
|
- Страница «Логи Hysteria» читает записи из journald unit `hysteria-server.service`.
|
||||||
|
- Экспорт «Логи Hysteria» также формируется из journald (`journalctl`), а не из отдельного файла `hysteria2.log`.
|
||||||
|
|
||||||
Проверка install-state marker:
|
Проверка install-state marker:
|
||||||
```bash
|
```bash
|
||||||
cat /var/lib/hy2xs/install-state.json
|
cat /var/lib/hy2xs/install-state.json
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Wants=network-online.target
|
|||||||
Type=simple
|
Type=simple
|
||||||
User=hy2xs-admin
|
User=hy2xs-admin
|
||||||
Group=hy2xs-admin
|
Group=hy2xs-admin
|
||||||
|
SupplementaryGroups=systemd-journal
|
||||||
WorkingDirectory={{INSTALL_DIR}}
|
WorkingDirectory={{INSTALL_DIR}}
|
||||||
EnvironmentFile=/etc/hy2xs/hy2xs.env
|
EnvironmentFile=/etc/hy2xs/hy2xs.env
|
||||||
Environment=GIN_MODE=release
|
Environment=GIN_MODE=release
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Wants=network-online.target
|
|||||||
Type=simple
|
Type=simple
|
||||||
User=hysteria
|
User=hysteria
|
||||||
Group=hysteria
|
Group=hysteria
|
||||||
|
Environment=HYSTERIA_LOG_LEVEL=info
|
||||||
|
Environment=HYSTERIA_LOG_FORMAT=json
|
||||||
ExecStart=/usr/local/bin/hysteria server -c /etc/hysteria/config.yaml
|
ExecStart=/usr/local/bin/hysteria server -c /etc/hysteria/config.yaml
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|||||||
Reference in New Issue
Block a user