From 84485eae1601a2969c83fa8fecac699d1865ee9e Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Sun, 14 Jan 2024 12:11:59 +0800 Subject: [PATCH] feat: admin service logger page --- adapter/baichuan/processor.go | 1 - adapter/midjourney/expose.go | 5 +- admin/controller.go | 35 +++++++ admin/logger.go | 49 ++++++++++ admin/market.go | 3 +- admin/router.go | 5 + app/src-tauri/tauri.conf.json | 2 +- app/src/admin/api/logger.ts | 55 +++++++++++ app/src/assets/admin/all.less | 3 + app/src/assets/admin/logger.less | 127 +++++++++++++++++++++++++ app/src/assets/globals.less | 3 + app/src/components/Paragraph.tsx | 3 + app/src/components/admin/MenuBar.tsx | 6 ++ app/src/conf.ts | 2 +- app/src/resources/i18n/cn.json | 5 + app/src/resources/i18n/en.json | 7 +- app/src/resources/i18n/ja.json | 7 +- app/src/resources/i18n/ru.json | 7 +- app/src/router.tsx | 15 +++ app/src/routes/admin/Logger.tsx | 135 +++++++++++++++++++++++++++ app/src/utils/base.ts | 7 ++ channel/channel.go | 1 - connection/cache.go | 8 +- connection/database.go | 14 +-- globals/logger.go | 11 ++- manager/conversation/storage.go | 3 +- manager/types.go | 1 - utils/fs.go | 89 +++++++++++++++++- 28 files changed, 579 insertions(+), 30 deletions(-) create mode 100644 admin/logger.go create mode 100644 app/src/admin/api/logger.ts create mode 100644 app/src/assets/admin/logger.less create mode 100644 app/src/routes/admin/Logger.tsx diff --git a/adapter/baichuan/processor.go b/adapter/baichuan/processor.go index a762e29..f7d6935 100644 --- a/adapter/baichuan/processor.go +++ b/adapter/baichuan/processor.go @@ -80,7 +80,6 @@ func (c *ChatInstance) ProcessLine(buf, data string) (string, error) { return "", nil } - fmt.Println(item) if form := processChatResponse(item); form == nil { // recursive call if len(buf) > 0 { diff --git a/adapter/midjourney/expose.go b/adapter/midjourney/expose.go index b7bfac3..e0ea9e7 100644 --- a/adapter/midjourney/expose.go +++ b/adapter/midjourney/expose.go @@ -1,6 +1,7 @@ package midjourney import ( + "chat/globals" "chat/utils" "fmt" "github.com/gin-gonic/gin" @@ -31,7 +32,7 @@ func InWhiteList(ip string) bool { func NotifyAPI(c *gin.Context) { if !InWhiteList(c.ClientIP()) { - fmt.Println(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP())) + globals.Info(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP())) c.AbortWithStatus(http.StatusForbidden) return } @@ -41,7 +42,7 @@ func NotifyAPI(c *gin.Context) { c.AbortWithStatus(http.StatusBadRequest) return } - // fmt.Println(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP())) + globals.Debug(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP())) if !utils.Contains(form.Status, []string{InProgress, Success, Failure}) { // ignore diff --git a/admin/controller.go b/admin/controller.go index 6f298c7..03ea9d7 100644 --- a/admin/controller.go +++ b/admin/controller.go @@ -215,3 +215,38 @@ func UpdateRootPasswordAPI(c *gin.Context) { "status": true, }) } + +func ListLoggerAPI(c *gin.Context) { + c.JSON(http.StatusOK, ListLogs()) +} + +func DownloadLoggerAPI(c *gin.Context) { + path := c.Query("path") + getBlobFile(c, path) +} + +func DeleteLoggerAPI(c *gin.Context) { + path := c.Query("path") + if err := deleteLogFile(path); err != nil { + c.JSON(http.StatusOK, gin.H{ + "status": false, + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "status": true, + }) +} + +func ConsoleLoggerAPI(c *gin.Context) { + n := utils.ParseInt(c.Query("n")) + + content := getLatestLogs(n) + + c.JSON(http.StatusOK, gin.H{ + "status": true, + "content": content, + }) +} diff --git a/admin/logger.go b/admin/logger.go new file mode 100644 index 0000000..187d2a2 --- /dev/null +++ b/admin/logger.go @@ -0,0 +1,49 @@ +package admin + +import ( + "chat/globals" + "chat/utils" + "fmt" + "github.com/gin-gonic/gin" + "strings" +) + +type LogFile struct { + Path string `json:"path"` + Size int64 `json:"size"` +} + +func ListLogs() []LogFile { + return utils.Each(utils.Walk("logs"), func(path string) LogFile { + return LogFile{ + Path: strings.TrimLeft(path, "logs/"), + Size: utils.GetFileSize(path), + } + }) +} + +func getLogPath(path string) string { + return fmt.Sprintf("logs/%s", path) +} + +func getBlobFile(c *gin.Context, path string) { + c.File(getLogPath(path)) +} + +func deleteLogFile(path string) error { + return utils.DeleteFile(getLogPath(path)) +} + +func getLatestLogs(n int) string { + if n <= 0 { + n = 100 + } + + content, err := utils.ReadFileLatestLines(getLogPath(globals.DefaultLoggerFile), n) + + if err != nil { + return fmt.Sprintf("read error: %s", err.Error()) + } + + return content +} diff --git a/admin/market.go b/admin/market.go index 70829fe..72a55d3 100644 --- a/admin/market.go +++ b/admin/market.go @@ -1,6 +1,7 @@ package admin import ( + "chat/globals" "fmt" "github.com/spf13/viper" ) @@ -24,7 +25,7 @@ type Market struct { func NewMarket() *Market { var models MarketModelList if err := viper.UnmarshalKey("market", &models); err != nil { - fmt.Println(fmt.Sprintf("[market] read config error: %s, use default config", err.Error())) + globals.Warn(fmt.Sprintf("[market] read config error: %s, use default config", err.Error())) models = MarketModelList{} } diff --git a/admin/router.go b/admin/router.go index 554414a..3331041 100644 --- a/admin/router.go +++ b/admin/router.go @@ -26,4 +26,9 @@ func Register(app *gin.RouterGroup) { app.POST("/admin/user/root", UpdateRootPasswordAPI) app.POST("/admin/market/update", UpdateMarketAPI) + + app.GET("/admin/logger/list", ListLoggerAPI) + app.GET("/admin/logger/download", DownloadLoggerAPI) + app.GET("/admin/logger/console", ConsoleLoggerAPI) + app.POST("/admin/logger/delete", DeleteLoggerAPI) } diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index dc6e49f..3ae5b59 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "chatnio", - "version": "3.8.5" + "version": "3.8.6" }, "tauri": { "allowlist": { diff --git a/app/src/admin/api/logger.ts b/app/src/admin/api/logger.ts new file mode 100644 index 0000000..e8fe334 --- /dev/null +++ b/app/src/admin/api/logger.ts @@ -0,0 +1,55 @@ +import axios from "axios"; +import { CommonResponse } from "@/admin/utils.ts"; +import { getErrorMessage } from "@/utils/base.ts"; + +export type Logger = { + path: string; + size: number; +}; + +export async function listLoggers(): Promise { + try { + const response = await axios.get("/admin/logger/list"); + return response.data as Logger[]; + } catch (e) { + console.warn(e); + return []; + } +} + +export async function getLoggerConsole(n?: number): Promise { + try { + const response = await axios.get(`/admin/logger/console?n=${n ?? 100}`); + return response.data.content as string; + } catch (e) { + console.warn(e); + return `failed to get info from server: ${getErrorMessage(e)}`; + } +} + +export async function downloadLogger(path: string): Promise { + try { + const response = await axios.get("/admin/logger/download", { + responseType: "blob", + params: { path }, + }); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", path); + document.body.appendChild(link); + link.click(); + } catch (e) { + console.warn(e); + } +} + +export async function deleteLogger(path: string): Promise { + try { + const response = await axios.post(`/admin/logger/delete?path=${path}`); + return response.data as CommonResponse; + } catch (e) { + console.warn(e); + return { status: false, error: getErrorMessage(e) }; + } +} diff --git a/app/src/assets/admin/all.less b/app/src/assets/admin/all.less index bfd0e0f..657446d 100644 --- a/app/src/assets/admin/all.less +++ b/app/src/assets/admin/all.less @@ -7,6 +7,8 @@ @import "charge"; @import "system"; @import "subscription"; +@import "logger"; + .admin-page { position: relative; @@ -33,6 +35,7 @@ .channel, .charge, .system, + .logger, .admin-subscription { padding: 0 !important; diff --git a/app/src/assets/admin/logger.less b/app/src/assets/admin/logger.less new file mode 100644 index 0000000..a4e986f --- /dev/null +++ b/app/src/assets/admin/logger.less @@ -0,0 +1,127 @@ +.logger { + width: 100%; + height: max-content; + padding: 2rem; + display: flex; + flex-direction: column; + + .logger-card { + width: 100%; + height: 100%; + min-height: 20vh; + } +} + +.logger-container { + .paragraph-header { + margin-bottom: 0.5rem; + } +} + +.logger-toolbar { + display: flex; + flex-direction: row; + align-items: center; + + input { + max-width: 4.5rem; + text-align: center; + } + + button { + flex-shrink: 0; + } + + & > * { + margin-right: 0.5rem !important; + white-space: nowrap; + + &:last-child { + margin-right: 0; + } + } +} + +.logger-console { + position: relative; + border-radius: var(--radius); + background-color: hsl(var(--background-dark)); + color: hsl(var(--text-light)); + font-size: 14px; + width: 100%; + height: max-content; + overflow: hidden; + + + pre { + width: 100%; + height: max-content; + min-height: 20vh; + max-height: 60vh; + overflow-x: hidden; + overflow-y: auto; + touch-action: pan-y; + padding: 0.5rem; + } + + .console-icon { + position: absolute; + top: 0.75rem; + right: 0.75rem; + user-select: none; + } +} + +.logger-list { + display: flex; + flex-direction: column; + width: 100%; + height: max-content; + + & > * { + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + } +} + +.logger-item { + display: flex; + flex-direction: row; + padding: 0.75rem 1rem; + flex-wrap: wrap; + border-radius: var(--radius); + border: 1px solid hsl(var(--border)); + transition: all 0.2s ease-in-out; + align-items: center; + + &:hover { + border-color: hsl(var(--border-hover)); + } + + & > * { + margin-right: 1rem; + flex-shrink: 0; + white-space: nowrap; + + &:last-child { + margin-right: 0; + } + } + + .logger-item-title { + font-size: 16px; + color: hsl(var(--text)); + } + + .logger-item-size { + font-size: 14px; + color: hsl(var(--text-secondary)); + } + + .logger-item-action { + cursor: pointer; + } +} diff --git a/app/src/assets/globals.less b/app/src/assets/globals.less index 18bcf59..46cd754 100644 --- a/app/src/assets/globals.less +++ b/app/src/assets/globals.less @@ -5,6 +5,8 @@ @layer base { :root { --background: 0 0% 100%; + --background-light: 0 0% 100%; + --background-dark: 0 0% 0%; --background-hover: 5 12% 100%; --background-container: 0, 0%, 97%, 0.8; --foreground: 240 10% 3.9%; @@ -42,6 +44,7 @@ --ring: 240 5% 64.9%; --text: 0 0% 0%; + --text-light: 0 0% 100%; --text-dark: 0 0% 100%; --text-secondary: 0 0% 35%; --text-secondary-dark: 0 0% 80%; diff --git a/app/src/components/Paragraph.tsx b/app/src/components/Paragraph.tsx index 33c66ee..98d64c1 100644 --- a/app/src/components/Paragraph.tsx +++ b/app/src/components/Paragraph.tsx @@ -7,6 +7,7 @@ import Markdown from "@/components/Markdown.tsx"; export type ParagraphProps = { title?: string; children: React.ReactNode; + className?: string; configParagraph?: boolean; isCollapsed?: boolean; onCollapse?: () => void; @@ -16,6 +17,7 @@ export type ParagraphProps = { function Paragraph({ title, children, + className, configParagraph, isCollapsed, onCollapse, @@ -32,6 +34,7 @@ function Paragraph({ configParagraph && `config-paragraph`, isCollapsed && `collapsable`, collapsed && `collapsed`, + className, )} >
} path={"/system"} /> + } + path={"/logger"} + />
); } diff --git a/app/src/conf.ts b/app/src/conf.ts index 37261c5..40de0ed 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -14,7 +14,7 @@ import React from "react"; import { syncSiteInfo } from "@/admin/api/info.ts"; import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts"; -export const version = "3.8.5"; +export const version = "3.8.6"; export const dev: boolean = getDev(); export const deploy: boolean = true; export let rest_api: string = getRestApi(deploy); diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index bf3f853..1ef0d19 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -518,6 +518,11 @@ "searchEndpoint": "搜索接入点", "searchQuery": "最大搜索结果数", "searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。" + }, + "logger": { + "title": "服务日志", + "console": "控制台", + "consoleLength": "日志条数" } }, "mask": { diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index 4e65c22..0a94ab9 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -478,7 +478,12 @@ "update": "Update" }, "model-chart-tip": "Token usage", - "subscription": "Subscription Management" + "subscription": "Subscription Management", + "logger": { + "title": "service log", + "console": "Console", + "consoleLength": "Number of log entries" + } }, "mask": { "title": "Mask Settings", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index e434770..4c11b73 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -478,7 +478,12 @@ "update": "更新" }, "model-chart-tip": "トークンの使用状況", - "subscription": "サブスクリプション管理" + "subscription": "サブスクリプション管理", + "logger": { + "title": "サービスログ", + "console": "コンソール", + "consoleLength": "ログエントリの数" + } }, "mask": { "title": "プリセット設定", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 85435d0..1184b9e 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -478,7 +478,12 @@ "update": "Обновить" }, "model-chart-tip": "Использование токенов", - "subscription": "Управление подписками" + "subscription": "Управление подписками", + "logger": { + "title": "Журнал обслуживания", + "console": "Консоль", + "consoleLength": "Количество записей в журнале" + } }, "mask": { "title": "Настройки маски", diff --git a/app/src/router.tsx b/app/src/router.tsx index ed5d256..3789e79 100644 --- a/app/src/router.tsx +++ b/app/src/router.tsx @@ -30,6 +30,7 @@ const Broadcast = lazyFactor(() => import("@/routes/admin/Broadcast.tsx")); const Subscription = lazyFactor( () => import("@/routes/admin/Subscription.tsx"), ); +const Logger = lazyFactor(() => import("@/routes/admin/Logger.tsx")); const router = createBrowserRouter( [ @@ -188,9 +189,23 @@ const router = createBrowserRouter( ), }, + { + id: "admin-logger", + path: "logger", + element: ( + + + + ), + }, ], ErrorBoundary: NotFound, }, + { + id: "not-found", + path: "*", + element: , + }, ].filter(Boolean), ); diff --git a/app/src/routes/admin/Logger.tsx b/app/src/routes/admin/Logger.tsx new file mode 100644 index 0000000..ad983d4 --- /dev/null +++ b/app/src/routes/admin/Logger.tsx @@ -0,0 +1,135 @@ +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card.tsx"; +import { useTranslation } from "react-i18next"; +import { useMemo, useState } from "react"; +import { useEffectAsync } from "@/utils/hook.ts"; +import { + Logger, + listLoggers, + downloadLogger, + deleteLogger, + getLoggerConsole, +} from "@/admin/api/logger.ts"; +import { getSizeUnit } from "@/utils/base.ts"; +import { Download, RotateCcw, Terminal, Trash } from "lucide-react"; +import { toastState } from "@/admin/utils.ts"; +import { useToast } from "@/components/ui/use-toast.ts"; +import Paragraph from "@/components/Paragraph.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { NumberInput } from "@/components/ui/number-input.tsx"; +import { Button } from "@/components/ui/button.tsx"; + +type LoggerItemProps = Logger & { + onUpdate: () => void; +}; +function LoggerItem({ path, size, onUpdate }: LoggerItemProps) { + const { t } = useTranslation(); + const { toast } = useToast(); + const loggerSize = useMemo(() => getSizeUnit(size), [size]); + + return ( +
+
{path}
+
+
{loggerSize}
+
downloadLogger(path)} + > + +
+
+ { + const resp = await deleteLogger(path); + if (resp) onUpdate(); + toastState(toast, t, resp, true); + }} + /> +
+
+ ); +} + +function LoggerList() { + const [data, setData] = useState([]); + + const sync = async () => setData(await listLoggers()); + + useEffectAsync(async () => { + await sync(); + }, []); + + return ( +
+ {data.map((logger, i) => ( + + ))} +
+ ); +} + +function LoggerConsole() { + const { t } = useTranslation(); + const [data, setData] = useState(""); + const [loading, setLoading] = useState(false); + const [length, setLength] = useState(100); + + const sync = async () => { + if (loading) return; + setLoading(true); + setData(await getLoggerConsole(length)); + setLoading(false); + }; + useEffectAsync(sync, []); + + return ( + +
+ + +
+ +
+
+ +
{data}
+
+ + ); +} + +function Logger() { + const { t } = useTranslation(); + return ( +
+ + + {t("admin.logger.title")} + + + + + + +
+ ); +} + +export default Logger; diff --git a/app/src/utils/base.ts b/app/src/utils/base.ts index abab538..9dcd059 100644 --- a/app/src/utils/base.ts +++ b/app/src/utils/base.ts @@ -88,3 +88,10 @@ export function resetJsArray(arr: T[], target: T[]): T[] { arr.splice(0, arr.length, ...target); return arr; } + +export function getSizeUnit(size: number): string { + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`; + if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`; + return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`; +} diff --git a/channel/channel.go b/channel/channel.go index c25abaf..d042692 100644 --- a/channel/channel.go +++ b/channel/channel.go @@ -180,7 +180,6 @@ func (c *Channel) ProcessError(err error) error { } content := err.Error() - fmt.Println(content) if strings.Contains(content, c.GetEndpoint()) { // hide the endpoint replacer := fmt.Sprintf("channel://%d", c.GetId()) diff --git a/connection/cache.go b/connection/cache.go index 8c7df2d..30f6586 100644 --- a/connection/cache.go +++ b/connection/cache.go @@ -1,11 +1,11 @@ package connection import ( + "chat/globals" "context" "fmt" "github.com/go-redis/redis/v8" "github.com/spf13/viper" - "log" ) var Cache *redis.Client @@ -27,7 +27,7 @@ func ConnectRedis() *redis.Client { }) if err := pingRedis(Cache); err != nil { - log.Println( + globals.Warn( fmt.Sprintf( "[connection] failed to connect to redis host: %s (message: %s), will retry in 5 seconds", viper.GetString("redis.host"), @@ -35,12 +35,12 @@ func ConnectRedis() *redis.Client { ), ) } else { - log.Println(fmt.Sprintf("[connection] connected to redis (host: %s)", viper.GetString("redis.host"))) + globals.Debug(fmt.Sprintf("[connection] connected to redis (host: %s)", viper.GetString("redis.host"))) } if viper.GetBool("debug") { Cache.FlushAll(context.Background()) - log.Println(fmt.Sprintf("[connection] flush redis cache (host: %s)", viper.GetString("redis.host"))) + globals.Debug(fmt.Sprintf("[connection] flush redis cache (host: %s)", viper.GetString("redis.host"))) } return Cache } diff --git a/connection/database.go b/connection/database.go index b42ca3d..a4d178f 100644 --- a/connection/database.go +++ b/connection/database.go @@ -1,12 +1,12 @@ package connection import ( + "chat/globals" "chat/utils" "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" "github.com/spf13/viper" - "log" ) var DB *sql.DB @@ -30,7 +30,7 @@ func ConnectMySQL() *sql.DB { viper.GetString("mysql.db"), )) if err != nil || db.Ping() != nil { - log.Println( + globals.Warn( fmt.Sprintf("[connection] failed to connect to mysql server: %s (message: %s), will retry in 5 seconds", viper.GetString("mysql.host"), utils.GetError(err), // err.Error() may contain nil pointer @@ -42,7 +42,7 @@ func ConnectMySQL() *sql.DB { return ConnectMySQL() } else { - log.Println(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host"))) + globals.Debug(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host"))) } db.SetMaxOpenConns(512) @@ -69,21 +69,21 @@ func InitRootUser(db *sql.DB) { var count int err := db.QueryRow("SELECT COUNT(*) FROM auth").Scan(&count) if err != nil { - fmt.Println(err) + globals.Warn(fmt.Sprintf("[service] failed to query user count: %s", err.Error())) return } if count == 0 { - fmt.Println("[service] no user found, creating root user (username: root, password: chatnio123456, email: root@example.com)") + globals.Debug("[service] no user found, creating root user (username: root, password: chatnio123456, email: root@example.com)") _, err := db.Exec(` INSERT INTO auth (username, password, email, is_admin, bind_id, token) VALUES (?, ?, ?, ?, ?, ?) `, "root", utils.Sha2Encrypt("chatnio123456"), "root@example.com", true, 0, "root") if err != nil { - fmt.Println(err) + globals.Warn(fmt.Sprintf("[service] failed to create root user: %s", err.Error())) } } else { - fmt.Println(fmt.Sprintf("[service] %d user(s) found, skip creating root user", count)) + globals.Debug(fmt.Sprintf("[service] %d user(s) found, skip creating root user", count)) } } diff --git a/globals/logger.go b/globals/logger.go index 5c5ee73..6f1eeb5 100644 --- a/globals/logger.go +++ b/globals/logger.go @@ -4,9 +4,12 @@ import ( "fmt" "github.com/natefinch/lumberjack" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "strings" ) +const DefaultLoggerFile = "chatnio.log" + var Logger *logrus.Logger type AppLogger struct { @@ -21,6 +24,10 @@ func (l *AppLogger) Format(entry *logrus.Entry) ([]byte, error) { entry.Message, ) + if !viper.GetBool("log.ignore_console") { + fmt.Println(data) + } + return []byte(data), nil } @@ -31,10 +38,10 @@ func init() { }) Logger.SetOutput(&lumberjack.Logger{ - Filename: "logs/chat.log", + Filename: fmt.Sprintf("logs/%s", DefaultLoggerFile), MaxSize: 1, MaxBackups: 500, - MaxAge: 1, + MaxAge: 21, // 3 weeks }) Logger.SetLevel(logrus.DebugLevel) diff --git a/manager/conversation/storage.go b/manager/conversation/storage.go index 5c68572..e245b7f 100644 --- a/manager/conversation/storage.go +++ b/manager/conversation/storage.go @@ -6,7 +6,6 @@ import ( "chat/utils" "database/sql" "fmt" - "log" ) func (c *Conversation) SaveConversation(db *sql.DB) bool { @@ -28,7 +27,7 @@ func (c *Conversation) SaveConversation(db *sql.DB) bool { defer func(stmt *sql.Stmt) { err := stmt.Close() if err != nil { - log.Println(err) + globals.Warn(err) } }(stmt) diff --git a/manager/types.go b/manager/types.go index 644f192..1aa0016 100644 --- a/manager/types.go +++ b/manager/types.go @@ -132,7 +132,6 @@ func transformContent(content interface{}) string { func transform(m []Message) []globals.Message { var messages []globals.Message for _, v := range m { - fmt.Println(transformContent(v.Content)) messages = append(messages, globals.Message{ Role: v.Role, Content: transformContent(v.Content), diff --git a/utils/fs.go b/utils/fs.go index 66f94be..72fcb6e 100644 --- a/utils/fs.go +++ b/utils/fs.go @@ -1,6 +1,9 @@ package utils import ( + "bufio" + "chat/globals" + "errors" "fmt" "io" "os" @@ -42,10 +45,15 @@ func WriteFile(path string, data string, folderSafe bool) bool { if err != nil { return false } - defer file.Close() + defer func(file *os.File) { + err := file.Close() + if err != nil { + globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path)) + } + }(file) if _, err := file.WriteString(data); err != nil { - fmt.Println(err.Error()) + globals.Warn(fmt.Sprintf("[utils] write file error: %s (path: %s, bytes len: %d)", err.Error(), path, len(data))) return false } return true @@ -68,6 +76,44 @@ func Walk(path string) []string { return files } +func GetFileSize(path string) int64 { + file, err := os.Open(path) + if err != nil { + return 0 + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path)) + } + }(file) + + stat, err := file.Stat() + if err != nil { + return 0 + } + return stat.Size() +} + +func GetFileCreated(path string) string { + file, err := os.Open(path) + if err != nil { + return "" + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path)) + } + }(file) + + stat, err := file.Stat() + if err != nil { + return "" + } + return stat.ModTime().String() +} + func IsFileExist(path string) bool { _, err := os.Stat(path) return err == nil || os.IsExist(err) @@ -81,7 +127,7 @@ func CopyFile(src string, dst string) error { defer func(in *os.File) { err := in.Close() if err != nil { - fmt.Println(err) + globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), src)) } }(in) @@ -93,10 +139,45 @@ func CopyFile(src string, dst string) error { defer func(out *os.File) { err := out.Close() if err != nil { - fmt.Println(err) + globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), dst)) } }(out) _, err = io.Copy(out, in) return err } + +func DeleteFile(path string) error { + return os.Remove(path) +} + +func ReadFileLatestLines(path string, length int) (string, error) { + if length <= 0 { + return "", errors.New("length must be greater than 0") + } + + file, err := os.Open(path) + if err != nil { + return "", err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + globals.Warn(fmt.Sprintf("[utils] close file error: %s (path: %s)", err.Error(), path)) + } + }(file) + + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + var lines []string + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if len(lines) < length { + length = len(lines) + } + + return strings.Join(lines[len(lines)-length:], "\n"), nil +}