4e2cd43a55
iotlcm-onelink-mock:OneLink 挡板服务(本地测试替身)。 - Go module 迁移至 git.5iot.com/iotlcm-public/iotlcm-onelink-mock - 修复同毫秒内多次生成卡导致 ICCID/IMSI 唯一约束冲突 - 补 Apache-2.0 LICENSE,README 重写并新增 docs/deploy.md - 移除内部部署资产(CI 触发器 / 镜像推送脚本 / 指定环境部署文档) - 测试凭据改为 demo_appid / demo_password,真实手机号与内网 IP 脱敏 - 统一 LF 行尾并加 .gitattributes
130 lines
2.9 KiB
Go
130 lines
2.9 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const defaultRuntimeLogCapacity = 500
|
|
|
|
type RuntimeLog struct {
|
|
ID int64 `json:"id"`
|
|
CreatedAt string `json:"createdAt"`
|
|
Level string `json:"level"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type RuntimeLogResult struct {
|
|
Items []RuntimeLog `json:"items"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type RuntimeLogStore struct {
|
|
mu sync.Mutex
|
|
logs []RuntimeLog
|
|
partial []byte
|
|
nextID int64
|
|
capacity int
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewRuntimeLogStore(capacities ...int) *RuntimeLogStore {
|
|
capacity := defaultRuntimeLogCapacity
|
|
if len(capacities) > 0 {
|
|
capacity = capacities[0]
|
|
}
|
|
return newRuntimeLogStore(capacity, time.Now)
|
|
}
|
|
|
|
func newRuntimeLogStore(capacity int, now func() time.Time) *RuntimeLogStore {
|
|
if capacity < 1 {
|
|
capacity = 1
|
|
}
|
|
return &RuntimeLogStore{
|
|
logs: make([]RuntimeLog, 0, capacity),
|
|
capacity: capacity,
|
|
now: now,
|
|
}
|
|
}
|
|
|
|
func (s *RuntimeLogStore) Write(p []byte) (int, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
written := len(p)
|
|
s.partial = append(s.partial, p...)
|
|
for {
|
|
index := bytes.IndexByte(s.partial, '\n')
|
|
if index < 0 {
|
|
break
|
|
}
|
|
s.addLineLocked(string(bytes.TrimSuffix(s.partial[:index], []byte{'\r'})))
|
|
s.partial = append(s.partial[:0], s.partial[index+1:]...)
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
func (s *RuntimeLogStore) Latest(limit int) RuntimeLogResult {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if limit < 1 || limit > s.capacity {
|
|
limit = s.capacity
|
|
}
|
|
start := len(s.logs) - limit
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
items := make([]RuntimeLog, len(s.logs)-start)
|
|
for i := range items {
|
|
items[i] = s.logs[len(s.logs)-1-i]
|
|
}
|
|
return RuntimeLogResult{Items: items, Total: len(s.logs)}
|
|
}
|
|
|
|
func (s *RuntimeLogStore) addLineLocked(message string) {
|
|
message = strings.TrimSpace(message)
|
|
if message == "" {
|
|
return
|
|
}
|
|
s.nextID++
|
|
item := RuntimeLog{
|
|
ID: s.nextID,
|
|
CreatedAt: s.now().Format("2006-01-02 15:04:05"),
|
|
Level: runtimeLogLevel(message),
|
|
Message: message,
|
|
}
|
|
if len(s.logs) == s.capacity {
|
|
copy(s.logs, s.logs[1:])
|
|
s.logs[len(s.logs)-1] = item
|
|
return
|
|
}
|
|
s.logs = append(s.logs, item)
|
|
}
|
|
|
|
func runtimeLogLevel(message string) string {
|
|
upper := strings.ToUpper(message)
|
|
switch {
|
|
case strings.Contains(upper, "FATAL"), strings.Contains(upper, "PANIC"), strings.Contains(upper, "ERROR"), strings.Contains(upper, "[ERRO]"):
|
|
return "ERROR"
|
|
case strings.Contains(upper, "WARN"), strings.Contains(upper, "[WARN]"):
|
|
return "WARN"
|
|
case strings.Contains(upper, "DEBUG"), strings.Contains(upper, "[DEBU]"):
|
|
return "DEBUG"
|
|
default:
|
|
return "INFO"
|
|
}
|
|
}
|
|
|
|
func (s *Service) adminRuntimeLogs(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
writeJSON(w, http.StatusOK, s.runtimeLogs.Latest(limit))
|
|
}
|