chore: 初始开源发布
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
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user