Files
hotime/application.go
T
hoteas 332c2414bc feat(log): 访问日志携带 ua,挂 Seq 后控制台放宽到 Info+
- WebConnectLog 访问日志新增 ua 字段(User-Agent 按 rune 截断 200 字符),便于按设备归因
- SetSeqWriter 挂 Seq 后业务控制台由 Warn+ 放宽为 Info+,Debug/SQL 仍只进 Seq 与文件
- 新增 Logger.SetConsoleMinLevel,访问日志控制台单独保持 Warn+

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 01:35:45 +08:00

1095 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package hotime
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"io/ioutil"
stdlog "log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
. "code.hoteas.com/golang/hotime/cache"
"code.hoteas.com/golang/hotime/code"
. "code.hoteas.com/golang/hotime/common"
. "code.hoteas.com/golang/hotime/db"
"code.hoteas.com/golang/hotime/log"
mysql "github.com/go-sql-driver/mysql"
"github.com/rs/zerolog"
logrus "github.com/sirupsen/logrus"
)
type Application struct {
MakeCodeRouter map[string]*code.MakeCode
MethodRouter
Router
Log *log.Logger
WebConnectLog *log.Logger
Port string //端口号
TLSPort string //ssl访问端口号
connectListener []func(that *Context) bool //所有的访问监听,true按原计划继续使用,false表示有监听器处理
connectDbFunc func() (master, slave *sql.DB)
configPath string
Config Map
Db HoTimeDB
*HoTimeCache
*http.Server
http.Handler
shuttingDown atomic.Bool // 停机标志,置为 true 后新请求立即返回 503
inFlight atomic.Int64 // 当前正在处理的请求数(不含已返回 503 的)
shutdownOnce sync.Once // 保证多路触发(信号/关窗口)只执行一次
DrainTimeout time.Duration // 已停止接收新请求后、等老请求自然结束的最长时长,默认 5s(每 1s 检测在途请求,为 0 则立即进入 Shutdown)
ShutdownTimeout time.Duration // drain 超时后再给老请求的最长容忍时间,默认 10s(到期后强制关闭)
}
func (that *Application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if that.shuttingDown.Load() {
w.Header().Set("Connection", "close")
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
return
}
that.inFlight.Add(1)
defer that.inFlight.Add(-1)
that.handler(w, req)
}
// initiateGracefulShutdown 触发优雅停机,通过 sync.Once 保证多路触发只执行一次。
// drain: 已停止接收新请求后、等老请求自然结束的最长时长,每 1s 检测一次在途请求数,
// 为 0 则立即进入 Shutdown,避免无谓等待。
// shutdown: drain 超时后再给老请求的最长容忍时间,到期强制关闭。
func (that *Application) initiateGracefulShutdown(drain, shutdown time.Duration) {
that.shutdownOnce.Do(func() {
that.shuttingDown.Store(true)
that.Log.Infof("优雅停机:已停止接收新请求(后续请求一律返回 503),等待老请求自然结束,最多再等 %v...", drain)
// 每秒检测一次,若无在途请求立即停机,否则最多等满 drain
tick := time.NewTicker(1 * time.Second)
defer tick.Stop()
deadline := time.Now().Add(drain)
for {
n := that.inFlight.Load()
if n == 0 {
that.Log.Infof("已无在途请求,立即停机")
break
}
if time.Now().After(deadline) {
that.Log.Infof("已停止接收新请求,仍有 %d 个老请求未结束,将在最多 %v 后强制结束这些请求", n, shutdown)
break
}
that.Log.Infof("已停止接收新请求,当前仍有 %d 个老请求在执行,继续等待...", n)
<-tick.C
}
ctx, cancel := context.WithTimeout(context.Background(), shutdown)
defer cancel()
if err := that.Server.Shutdown(ctx); err != nil {
that.Log.Errorf("等待超时,强制关闭仍在执行的 %d 个请求: %v", that.inFlight.Load(), err)
} else {
that.Log.Infof("服务已安全关闭")
}
// 冲刷 Seq 队列,避免停机前后关键日志丢失
that.Log.CloseSeq()
if that.WebConnectLog != nil {
that.WebConnectLog.CloseSeq()
}
os.Exit(0)
})
}
// isLoopbackIP 判断是否为回环地址(空串视为不可用)
func isLoopbackIP(ip string) bool {
ip = strings.TrimSpace(ip)
if ip == "" {
return true
}
parsed := net.ParseIP(ip)
if parsed == nil {
return false
}
return parsed.IsLoopback()
}
// remoteHost 从 RemoteAddr 取出 host(去掉端口)
func remoteHost(req *http.Request) string {
if req == nil || req.RemoteAddr == "" {
return ""
}
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err == nil && host != "" {
return host
}
return req.RemoteAddr
}
// resolveClientIP 多源识别客户端 IP。
// bestEO-Connecting-IP → XFF 第一个非回环 → 非回环 X-Real-IP → RemoteAddr
// XFF 先于 X-Real:反代误把 CDN 节点写进 X-Real-IP 时仍能落到客户端)
// chain:各源头 IP 按首次出现顺序逗号拼接,同一 IP 只保留一次
func resolveClientIP(req *http.Request) (best string, chain string) {
if req == nil {
return "", ""
}
eo := strings.TrimSpace(req.Header.Get("EO-Connecting-IP"))
xri := strings.TrimSpace(req.Header.Get("X-Real-IP"))
xffRaw := req.Header.Get("X-Forwarded-For")
ra := remoteHost(req)
var xffParts []string
if xffRaw != "" {
for _, p := range strings.Split(xffRaw, ",") {
if p = strings.TrimSpace(p); p != "" {
xffParts = append(xffParts, p)
}
}
}
seen := make(map[string]struct{}, 4)
var ordered []string
add := func(ip string) {
ip = strings.TrimSpace(ip)
if ip == "" {
return
}
if _, ok := seen[ip]; ok {
return
}
seen[ip] = struct{}{}
ordered = append(ordered, ip)
}
add(eo)
add(xri)
for _, p := range xffParts {
add(p)
}
add(ra)
chain = strings.Join(ordered, ",")
if eo != "" && !isLoopbackIP(eo) {
return eo, chain
}
for _, p := range xffParts {
if !isLoopbackIP(p) {
return p, chain
}
}
if xri != "" && !isLoopbackIP(xri) {
return xri, chain
}
if ra != "" {
return ra, chain
}
return "", chain
}
// clientIP 取客户端真实 IP(见 resolveClientIP
func clientIP(req *http.Request) string {
ip, _ := resolveClientIP(req)
return ip
}
// truncateUA 按 rune 安全截断字符串到 max 长度,避免切坏多字节字符导致 JSON 转义问题
func truncateUA(s string, max int) string {
r := []rune(s)
if len(r) <= max {
return s
}
return string(r[:max])
}
// shortID 生成 n 字节随机 hexn=6 → 12 位)
func shortID(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return Md5(strconv.Itoa(Rand(10)))[:n*2]
}
return hex.EncodeToString(b)
}
// Run 启动实例
func (that *Application) Run(router Router) {
//如果没有设置配置自动生成配置
if that.configPath == "" || len(that.Config) == 0 {
that.SetConfig()
}
//防止手动设置缓存误伤
if that.HoTimeCache == nil {
that.SetCache()
}
//防止手动设置session误伤
//if that.sessionShort == nil && that.sessionLong == nil {
// if that.connectDbFunc == nil {
// that.SetSession(CacheIns(&CacheMemory{}), nil)
// } else {
// that.SetSession(CacheIns(&CacheMemory{}), CacheIns(&CacheDb{Db: &that.Db, Time: that.Config.GetInt64("cacheLongTime")}))
// }
//
//}
//that.Router = router
if that.Router == nil {
that.Router = Router{}
}
for k, _ := range router {
v := router[k]
if that.Router[k] == nil {
that.Router[k] = v
}
//直达接口层复用
for k1, _ := range v {
v1 := v[k1]
if that.Router[k][k1] == nil {
that.Router[k][k1] = v1
}
for k2, _ := range v1 {
v2 := v1[k2]
that.Router[k][k1][k2] = v2
}
}
}
//重新设置MethodRouter//直达路由
that.MethodRouter = MethodRouter{}
modeRouterStrict := true
if that.Config.GetBool("modeRouterStrict") == false {
modeRouterStrict = false
}
if that.Router != nil {
for pk, pv := range that.Router {
if !modeRouterStrict {
pk = strings.ToLower(pk)
}
if pv != nil {
for ck, cv := range pv {
if !modeRouterStrict {
ck = strings.ToLower(ck)
}
if cv != nil {
for mk, mv := range cv {
if !modeRouterStrict {
mk = strings.ToLower(mk)
}
that.MethodRouter["/"+pk+"/"+ck+"/"+mk] = mv
}
}
}
}
}
}
//that.Port = port
that.Port = that.Config.GetString("port")
that.TLSPort = that.Config.GetString("tlsPort")
if that.connectDbFunc != nil && (that.Db.DB == nil || that.Db.DB.Ping() != nil) {
that.Db.SetConnect(that.connectDbFunc)
}
//异常处理
defer func() {
if err := recover(); err != nil {
if that.shuttingDown.Load() {
return // 停机过程中的 panic 不触发重启
}
//that.SetError(errors.New(fmt.Sprint(err)), LOG_FMT)
that.Log.EmitRecoveredPanic(err)
that.Run(router)
}
}()
that.Server = &http.Server{}
if !IsRun {
IsRun = true
}
ch := make(chan int)
if ObjToCeilInt(that.Port) != 0 {
go func() {
App[that.Port] = that
that.Server.Handler = that
//启动服务
that.Server.Addr = ":" + that.Port
err := that.Server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
that.Log.Error().Err(err).Msg("HTTP 服务退出")
ch <- 1
}
}()
}
if ObjToCeilInt(that.TLSPort) != 0 {
go func() {
App[that.TLSPort] = that
that.Server.Handler = that
//启动服务
that.Server.Addr = ":" + that.TLSPort
err := that.Server.ListenAndServeTLS(that.Config.GetString("tlsCert"), that.Config.GetString("tlsKey"))
if err != nil && err != http.ErrServerClosed {
that.Log.Error().Err(err).Msg("HTTPS 服务退出")
ch <- 2
}
}()
}
if ObjToCeilInt(that.Port) == 0 && ObjToCeilInt(that.TLSPort) == 0 {
that.Log.Errorf("没有端口启用")
return
}
// 注册 Windows 关窗口处理(Linux/macOS 上是空实现)
that.registerWindowsCloseHandler()
// 跨平台信号监听:Ctrl+C / SIGTERMkill 命令)/ taskkill /PID
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
drain := that.DrainTimeout
if drain == 0 {
drain = 5 * time.Second
}
shutdown := that.ShutdownTimeout
if shutdown == 0 {
shutdown = 10 * time.Second
}
that.initiateGracefulShutdown(drain, shutdown)
}()
value := <-ch
that.Log.Errorf("启动服务失败 : %s", ObjToStr(value))
}
// SetConnectDB 启动实例
func (that *Application) SetConnectDB(connect func() (master, slave *sql.DB)) {
that.connectDbFunc = connect
that.Db.SetConnect(that.connectDbFunc)
}
// SetDefault 默认配置缓存和session实现
func (that *Application) SetDefault(connect func() (*sql.DB, *sql.DB)) {
that.SetConfig()
if connect != nil {
that.connectDbFunc = connect
that.Db.SetConnect(that.connectDbFunc)
}
}
// SetCache 设置配置文件路径全路径或者相对路径
func (that *Application) SetCache() {
cacheIns := HoTimeCache{}
cacheIns.Init(that.Config.GetMap("cache"), HoTimeDBInterface(&that.Db), that.Log)
that.HoTimeCache = &cacheIns
that.Db.HoTimeCache = &cacheIns
}
// SetConfig 设置配置文件路径全路径或者相对路径
func (that *Application) SetConfig(configPath ...string) {
that.Log = log.NewLogger(1, "", 100)
if len(configPath) != 0 {
that.configPath = configPath[0]
}
if that.configPath == "" {
that.configPath = "config/config.json"
}
//加载配置文件
btes, err := ioutil.ReadFile(that.configPath)
that.Config = DeepCopyMap(Config).(Map)
configErr := false
if err == nil {
cmap := Map{}
cmap.JsonToMap(string(btes))
for k, v := range cmap {
that.Config[k] = v
Config[k] = v
}
} else {
that.Log.Errorf("配置文件不存在,或者配置出错,使用缺省默认配置")
}
if !configErr {
configStr := that.Config.ToJsonString()
if len(btes) != 0 && configStr == string(btes) {
// 配置无变化,跳过写入
} else {
configNoteStr := ConfigNote.ToJsonString()
_ = os.MkdirAll(filepath.Dir(that.configPath), os.ModeDir)
err = ioutil.WriteFile(that.configPath, []byte(configStr), os.ModePerm)
if err != nil {
that.Log.Error().Err(err).Msg("写入配置文件失败")
configErr = true
}
_ = ioutil.WriteFile(filepath.Dir(that.configPath)+"/configNote.json", []byte(configNoteStr), os.ModePerm)
}
}
logLevel := that.Config.GetCeilInt("logLevel")
logFile := that.Config.GetString("logFile")
logHistory := that.Config.GetCeilInt("logHistory")
if logHistory == 0 {
logHistory = 100
}
that.Log = log.NewLogger(logLevel, logFile, logHistory)
that.Db.Log = that.Log
if that.Config.Get("webConnectLogShow") == nil || that.Config.GetBool("webConnectLogShow") {
that.WebConnectLog = log.NewLoggerNoCaller(1, that.Config.GetString("webConnectLogFile"), 0)
}
// 重定向 os.Stdout 和标准 log 包,捕获 fmt.Println 等绕过 HoTime Logger 的输出
stdoutW := redirectStdout(that.Log)
bridgeThirdPartyStdout(stdoutW)
// 重定向 os.Stderr,捕获 panic 栈、MySQL driver errLog 等写 stderr 的输出
// 注意:Logger 在此之前已创建,ConsoleWriter.Out 已绑定到原始 os.Stderr,无循环风险
redirectStderr(that.Log)
// 接入 Seqinstance = ip:port,同机多进程靠端口区分,跨机器靠 IP 区分
if seqUrl := that.Config.GetString("seqUrl"); seqUrl != "" {
instance := getLocalIP() + ":" + that.Config.GetString("port")
that.Log.SetSeqWriter(seqUrl, that.Config.GetString("seqApiKey"), instance)
if that.WebConnectLog != nil {
that.WebConnectLog.SetSeqWriter(seqUrl, that.Config.GetString("seqApiKey"), instance)
// 访问日志量大,挂 Seq 后控制台仍保持 Warn+(业务 Logger 已放宽到 Info+
that.WebConnectLog.SetConsoleMinLevel(zerolog.WarnLevel)
}
that.Log.Infof("Seq 日志推送已启动: url=%s instance=%s", seqUrl, instance)
}
// 把 MySQL 驱动内部 errLog(连接/包错误)接入 HoTime Logger,经 SeqWriter 推送到 Seq
_ = mysql.SetLogger(&mysqlLogAdapter{l: that.Log})
}
// SetConnectListener 连接判断,返回false继续传输至控制层,true则停止传输
func (that *Application) SetConnectListener(lis func(that *Context) (isFinished bool)) {
that.connectListener = append(that.connectListener, lis)
}
//网络错误
//func (that *Application) session(w http.ResponseWriter, req *http.Request) {
//
//}
// 序列化链接
func (that *Application) urlSer(url string) (string, []string) {
q := strings.Index(url, "?")
if q == -1 {
q = len(url)
}
o := Substr(url, 0, q)
r := strings.SplitN(o, "/", -1)
var s = make([]string, 0)
for i := 0; i < len(r); i++ {
if !strings.EqualFold("", r[i]) {
s = append(s, r[i])
}
}
return o, s
}
//访问
func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
nowUnixTime := time.Now()
_, s := that.urlSer(req.RequestURI)
//获取cookie
// 如果cookie存在直接将sessionId赋值为cookie.Value
// 如果cookie不存在就查找传入的参数中是否有token
// 如果token不存在就生成随机的sessionId
// 如果token存在就判断token是否在Session中有保存
// 如果有取出token并复制给cookie
// 没有保存就生成随机的session
cookie, err := req.Cookie(that.Config.GetString("sessionName"))
sessionId := Md5(strconv.Itoa(Rand(10)))
needSetCookie := ""
token := req.Header.Get("Authorization")
if len(token) != 32 {
token = req.FormValue("token")
}
//没有cookie或者cookie不等于token
//有token优先token
if len(token) == 32 {
sessionId = token
//没有token,则查阅session
if cookie == nil || cookie.Value != sessionId {
needSetCookie = sessionId
}
} else if err == nil && cookie.Value != "" {
sessionId = cookie.Value
//session也没有则判断是否创建cookie
} else {
needSetCookie = sessionId
}
unescapeUrl, err := url.QueryUnescape(req.RequestURI)
if err != nil {
unescapeUrl = req.RequestURI
}
// 请求级追踪:sid=sessionId 前 12 位(脱敏),request_id 回写响应头
requestId := shortID(6)
sid := sessionId
if len(sid) > 12 {
sid = sid[:12]
}
w.Header().Set("X-Request-Id", requestId)
reqLog := that.Log.WithFields("sid", sid, "request_id", requestId)
dbCopy := that.Db
dbCopy.Log = reqLog
//访问实例
context := Context{SessionIns: SessionIns{SessionId: sessionId, HoTimeCache: that.HoTimeCache},
Resp: w, Req: req, Application: that, RouterString: s, Config: that.Config, Db: &dbCopy,
HandlerStr: unescapeUrl, Logger: reqLog}
//header默认设置
header := w.Header()
header.Set("Content-Type", "text/html; charset=utf-8")
//url去掉参数并序列化
context.HandlerStr, context.RouterString = that.urlSer(context.HandlerStr)
//跨域设置
that.crossDomain(&context, needSetCookie)
defer func() {
//是否展示日志
if that.WebConnectLog != nil {
ip, ipChain := resolveClientIP(req)
evt := that.WebConnectLog.Info().
Str("ip", ip).
Str("method", context.Req.Method).
Str("sid", sid).
Str("request_id", requestId).
Float64("cost_ms", ObjToFloat64(time.Now().UnixNano()-nowUnixTime.UnixNano())/1000000.00).
Float64("size_kb", ObjToFloat64(context.DataSize)/1000.00)
if ipChain != "" && ipChain != ip {
evt = evt.Str("ip_chain", ipChain)
}
if country := req.Header.Get("EO-Client-IPCountry"); country != "" {
evt = evt.Str("ip_country", country)
}
if ua := req.Header.Get("User-Agent"); ua != "" {
evt = evt.Str("ua", truncateUA(ua, 200))
}
evt.Msg(context.HandlerStr)
}
}()
//访问拦截true继续false暂停
connectListenerLen := len(that.connectListener) - 1
for true {
if connectListenerLen < 0 {
break
}
if that.connectListener[connectListenerLen](&context) {
context.View()
return
}
connectListenerLen--
}
//接口服务
//验证接口严格模式
modeRouterStrict := that.Config.GetBool("modeRouterStrict")
tempHandlerStr := context.HandlerStr
if !modeRouterStrict {
tempHandlerStr = strings.ToLower(tempHandlerStr)
}
//执行接口
if that.MethodRouter[tempHandlerStr] != nil {
that.MethodRouter[tempHandlerStr](&context)
context.View()
return
}
//url赋值,静态文件必须使用原始路径(Linux文件系统大小写敏感)
path := that.Config.GetString("tpt") + context.HandlerStr
//判断是否为默认
if path[len(path)-1] == '/' {
defFile := that.Config.GetSlice("defFile")
for i := 0; i < len(defFile); i++ {
temp := path + defFile.GetString(i)
_, err := os.Stat(temp)
if err == nil {
path = temp
break
}
}
if path[len(path)-1] == '/' {
w.WriteHeader(404)
return
}
}
if strings.Contains(path, "/.") {
w.WriteHeader(404)
return
}
// 静态资源可缓存;logLevel 只控制日志/SQL,不再改 Cache-Control
delete(header, "Content-Type")
header.Set("Cache-Control", "public")
t := strings.LastIndex(path, ".")
if t != -1 {
tt := strings.ToLower(path[t:])
if MimeMaps[tt] != "" {
header.Add("Content-Type", MimeMaps[tt])
}
}
//w.Write(data)
http.ServeFile(w, req, path)
}
func (that *Application) crossDomain(context *Context, sessionId string) {
//没有跨域设置
if context.Config.GetString("crossDomain") == "" {
if sessionId != "" {
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
//context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
return
}
header := context.Resp.Header()
//不跨域,则不设置
remoteHost := context.Req.Host
if context.Config.GetString("port") == "80" || context.Config.GetString("port") == "443" {
remoteHost = remoteHost + ":" + context.Config.GetString("port")
}
if context.Config.GetString("crossDomain") != "auto" {
//不跨域,则不设置
if strings.Contains(context.Config.GetString("crossDomain"), remoteHost) {
if sessionId != "" {
http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
}
return
}
header.Set("Access-Control-Allow-Origin", that.Config.GetString("crossDomain"))
// 后端设置,2592000单位秒,这里是30天
header.Set("Access-Control-Max-Age", "2592000")
//header.Set("Access-Control-Allow-Origin", "*")
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
if sessionId != "" {
//跨域允许需要设置cookie的允许跨域https才有效果
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
return
}
origin := context.Req.Header.Get("Origin")
refer := context.Req.Header.Get("Referer")
if (origin != "" && strings.Contains(origin, remoteHost)) || strings.Contains(refer, remoteHost) {
if sessionId != "" {
//http.SetCookie(context.Resp, &http.Cookie{Name: that.Config.GetString("sessionName"), Value: sessionId, Path: "/"})
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
return
}
if origin != "" {
header.Set("Access-Control-Allow-Origin", origin)
//return
} else if refer != "" {
tempInt := 0
lastInt := strings.IndexFunc(refer, func(r rune) bool {
if r == '/' && tempInt > 8 {
return true
}
tempInt++
return false
})
if lastInt < 0 {
lastInt = len(refer)
}
refer = Substr(refer, 0, lastInt)
header.Set("Access-Control-Allow-Origin", refer)
//header.Set("Access-Control-Allow-Origin", "*")
}
header.Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE")
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Access-Control-Expose-Headers", "*")
header.Set("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Access-Token,Authorization,Cookie,Set-Cookie")
if sessionId != "" {
//跨域允许需要设置cookie的允许跨域https才有效果
context.Resp.Header().Set("Set-Cookie", that.Config.GetString("sessionName")+"="+sessionId+"; Path=/; SameSite=None; Secure")
}
}
// getLocalIP 通过 UDP dial 探测方式获取本机出口 IP(不发送任何数据,无副作用)。
// 用于构建 Seq instance 字段,格式为 ip:port,支持跨机器集群识别。
// 失败时返回 "unknown"。
func getLocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "unknown"
}
defer conn.Close()
return conn.LocalAddr().(*net.UDPAddr).IP.String()
}
// redirectStdout 重定向 os.Stdout 和标准 log 包到 HoTime Logger。
// 返回管道写端,供 logrus 等第三方库桥接。
func redirectStdout(l *log.Logger) *os.File {
r, w, err := os.Pipe()
if err != nil {
return nil
}
os.Stdout = w
stdlog.SetOutput(w)
go log.CaptureStream(l, zerolog.InfoLevel, "stdout", r)
return w
}
// bridgeThirdPartyStdout 将 import 时钉死原始 stdout 的第三方日志指到捕获管道。
func bridgeThirdPartyStdout(w *os.File) {
if w == nil {
return
}
logrus.SetOutput(w)
}
// redirectStderr 重定向 os.Stderr 到 HoTime Loggerlevel=error, source=stderr)。
func redirectStderr(l *log.Logger) {
r, w, err := os.Pipe()
if err != nil {
l.EmitCaptured(zerolog.WarnLevel, "stderr-capture", fmt.Sprintf("create pipe failed: %v", err))
return
}
os.Stderr = w
go log.CaptureStream(l, zerolog.ErrorLevel, "stderr", r)
}
// Init 初始化application
func Init(config string) *Application {
appIns := Application{}
//手动模式,
appIns.SetConfig(config)
SetDB(&appIns)
appIns.SetCache()
codeConfig := appIns.Config.GetSlice("codeConfig")
if codeConfig != nil {
for k, _ := range codeConfig {
codeMake := codeConfig.GetMap(k)
if codeMake == nil {
continue
}
//codeMake["table"] = k
if appIns.MakeCodeRouter == nil {
appIns.MakeCodeRouter = map[string]*code.MakeCode{}
}
if codeMake.GetString("name") == "" {
codeMake["name"] = codeMake.GetString("table")
}
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Log: appIns.Log}
appIns.MakeCodeRouter[codeMake.GetString("name")].Db2JSON(&appIns.Db, codeMake)
//接入动态代码层
if appIns.Router == nil {
appIns.Router = Router{}
}
//appIns.Router[codeMake.GetString("name")] = TptProject
appIns.Router[codeMake.GetString("name")] = Proj{}
for k2, _ := range TptProject {
if appIns.Router[codeMake.GetString("name")][k2] == nil {
appIns.Router[codeMake.GetString("name")][k2] = Ctr{}
}
for k3, _ := range TptProject[k2] {
v3 := TptProject[k2][k3]
appIns.Router[codeMake.GetString("name")][k2][k3] = v3
}
}
for k1, _ := range appIns.MakeCodeRouter[codeMake.GetString("name")].TableColumns {
if appIns.Router[codeMake.GetString("name")][k1] == nil {
appIns.Router[codeMake.GetString("name")][k1] = Ctr{}
}
for k2, _ := range appIns.Router[codeMake.GetString("name")]["hotimeCommon"] {
//golang毛病
v2 := appIns.Router[codeMake.GetString("name")]["hotimeCommon"][k2]
appIns.Router[codeMake.GetString("name")][k1][k2] = v2
}
}
setMakeCodeListener(codeMake.GetString("name"), &appIns)
}
}
return &appIns
}
// SetDB 智能数据库设置
func SetDB(appIns *Application) {
db := appIns.Config.GetMap("db")
dbSqlite := db.GetMap("sqlite")
dbMysql := db.GetMap("mysql")
dbDm := db.GetMap("dm")
if db != nil && dbSqlite != nil {
SetSqliteDB(appIns, dbSqlite)
}
if db != nil && dbMysql != nil {
SetMysqlDB(appIns, dbMysql)
}
if db != nil && dbDm != nil {
SetDmDB(appIns, dbDm)
}
}
func SetMysqlDB(appIns *Application, config Map) {
appIns.Db.Type = "mysql"
appIns.Db.DBName = config.GetString("name")
appIns.Db.Prefix = config.GetString("prefix")
appIns.Db.Log = appIns.Log
appIns.SetConnectDB(func() (master, slave *sql.DB) {
query := config.GetString("user") + ":" + config.GetString("password") +
"@tcp(" + config.GetString("host") + ":" + config.GetString("port") + ")/" + config.GetString("name") + "?charset=utf8mb4"
DB, e := sql.Open("mysql", query)
if e != nil {
appIns.Log.Error().Err(e).Msg("MySQL 主库连接失败")
}
master = DB
configSlave := config.GetMap("slave")
if configSlave != nil {
query := configSlave.GetString("user") + ":" + configSlave.GetString("password") +
"@tcp(" + config.GetString("host") + ":" + configSlave.GetString("port") + ")/" + configSlave.GetString("name") + "?charset=utf8mb4"
DB1, e := sql.Open("mysql", query)
if e != nil {
appIns.Log.Error().Err(e).Msg("MySQL 从库连接失败")
}
slave = DB1
}
return master, slave
})
}
func SetSqliteDB(appIns *Application, config Map) {
appIns.Db.Type = "sqlite"
appIns.Db.Prefix = config.GetString("prefix")
appIns.Db.Log = appIns.Log
appIns.SetConnectDB(func() (master, slave *sql.DB) {
db, e := sql.Open("sqlite3", config.GetString("path"))
if e != nil {
appIns.Log.Error().Err(e).Msg("SQLite 连接失败")
}
master = db
return master, slave
})
}
func SetDmDB(appIns *Application, config Map) {
appIns.Db.Type = "dm"
appIns.Db.DBName = config.GetString("name")
appIns.Db.Prefix = config.GetString("prefix")
appIns.Db.Log = appIns.Log
appIns.SetConnectDB(func() (master, slave *sql.DB) {
query := "dm://" + config.GetString("user") + ":" + config.GetString("password") +
"@" + config.GetString("host") + ":" + config.GetString("port") + "?schema=" + config.GetString("name")
DB, e := sql.Open("dm", query)
if e != nil {
appIns.Log.Error().Err(e).Msg("达梦主库连接失败")
}
master = DB
configSlave := config.GetMap("slave")
if configSlave != nil {
querySlave := "dm://" + configSlave.GetString("user") + ":" + configSlave.GetString("password") +
"@" + configSlave.GetString("host") + ":" + configSlave.GetString("port") + "?schema=" + configSlave.GetString("name")
DB1, e := sql.Open("dm", querySlave)
if e != nil {
appIns.Log.Error().Err(e).Msg("达梦从库连接失败")
}
slave = DB1
}
return master, slave
})
}
func setMakeCodeListener(name string, appIns *Application) {
appIns.SetConnectListener(func(context *Context) (isFinished bool) {
codeIns := appIns.MakeCodeRouter[name]
if len(context.RouterString) < 2 || appIns.MakeCodeRouter[context.RouterString[0]] == nil {
return isFinished
}
if len(context.RouterString) > 1 && context.RouterString[0] == name {
if context.RouterString[1] == "hotime" && context.RouterString[2] == "login" {
return isFinished
}
if context.RouterString[1] == "hotime" && context.RouterString[2] == "logout" {
return isFinished
}
if context.RouterString[1] == "hotime" && context.RouterString[2] == "config" {
return isFinished
}
if context.RouterString[1] == "hotime" && context.RouterString[2] == "wallpaper" {
return isFinished
}
if context.Session(codeIns.FileConfig.GetString("table")+"_id").Data == nil {
context.Display(2, "你还没有登录")
return true
}
}
if context.RouterString[0] != name {
return isFinished
}
if len(context.RouterString) < 2 || len(context.RouterString) > 3 ||
!(context.Router[context.RouterString[0]] != nil &&
context.Router[context.RouterString[0]][context.RouterString[1]] != nil) {
return isFinished
}
//排除无效操作
if len(context.RouterString) == 2 &&
context.Req.Method != "GET" &&
context.Req.Method != "POST" {
return isFinished
}
//排除已有接口的无效操作
if len(context.RouterString) == 3 && context.Router[context.RouterString[0]] != nil && context.Router[context.RouterString[0]][context.RouterString[1]] != nil && context.Router[context.RouterString[0]][context.RouterString[1]][context.RouterString[2]] != nil {
return isFinished
}
//列表检索
if len(context.RouterString) == 2 &&
context.Req.Method == "GET" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["search"] == nil {
return isFinished
}
context.Router[context.RouterString[0]][context.RouterString[1]]["search"](context)
}
//新建
if len(context.RouterString) == 2 &&
context.Req.Method == "POST" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["add"] == nil {
return true
}
context.Router[context.RouterString[0]][context.RouterString[1]]["add"](context)
}
if len(context.RouterString) == 3 &&
context.Req.Method == "POST" {
return isFinished
}
//分析
if len(context.RouterString) == 3 && context.RouterString[2] == "analyse" &&
context.Req.Method == "GET" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["analyse"] == nil {
return isFinished
}
context.Router[context.RouterString[0]][context.RouterString[1]]["analyse"](context)
}
//查询单条
if len(context.RouterString) == 3 &&
context.Req.Method == "GET" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["info"] == nil {
return isFinished
}
context.Router[context.RouterString[0]][context.RouterString[1]]["info"](context)
}
//更新
if len(context.RouterString) == 3 &&
context.Req.Method == "PUT" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["update"] == nil {
return true
}
context.Router[context.RouterString[0]][context.RouterString[1]]["update"](context)
}
//移除
if len(context.RouterString) == 3 &&
context.Req.Method == "DELETE" {
if context.Router[context.RouterString[0]][context.RouterString[1]]["remove"] == nil {
return true
}
context.Router[context.RouterString[0]][context.RouterString[1]]["remove"](context)
}
//context.View()
return true
})
}
// mysqlLogAdapter 将 go-sql-driver/mysql 的内部 errLog 桥接到 HoTime Logger。
// MySQL 驱动连接/包级别错误通过此适配器以 error 级别推送到 Seq。
type mysqlLogAdapter struct{ l *log.Logger }
func (a *mysqlLogAdapter) Print(v ...interface{}) {
a.l.Error().Str("source", "mysql-driver").Msg(fmt.Sprint(v...))
}