Files
LokiStack/backend/controllers/logs.go
T
opsx f3733f0bfc feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏
- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
2026-10-27 17:11:38 +08:00

320 lines
9.5 KiB
Go

package controllers
import (
"context"
"strconv"
"strings"
"time"
"lokistack/dto"
"lokistack/models"
"lokistack/services"
"lokistack/utils"
"github.com/gin-gonic/gin"
)
func (ctrl *LogsController) Context(c *gin.Context) {
var req dto.LogContextRequest
if !utils.BindAndValidate(c, &req) {
return
}
src := services.GetLogSource(req.SourceID)
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
ts, err := strconv.ParseInt(req.TimestampNs, 10, 64)
if err != nil || ts < 1 {
utils.BadRequest(c, "时间戳无效")
return
}
selector, err := services.ContextLogSelector(req.Labels)
if err != nil {
utils.BadRequest(c, err.Error())
return
}
query, err := services.AuthorizedLogQuery(src, c.GetUint("userId"), c.GetString("role") == "admin", selector)
if err != nil {
utils.Forbidden(c, err.Error())
return
}
span := int64(5 * time.Minute)
before, err := services.QueryLokiLogs(c.Request.Context(), src, query, ts-span, ts-1, 5, "backward", "")
if err != nil {
utils.Error(c, 502, err.Error())
return
}
after, err := services.QueryLokiLogs(c.Request.Context(), src, query, ts+1, ts+span, 5, "forward", "")
if err != nil {
utils.Error(c, 502, err.Error())
return
}
utils.Success(c, gin.H{"before": before.Entries, "after": after.Entries})
}
// LogsController 日志查询代理 + 数据源管理
type LogsController struct{}
func NewLogsController() *LogsController {
return &LogsController{}
}
// ==================== 数据源 ====================
func (ctrl *LogsController) ListSources(c *gin.Context) {
if c.GetString("role") == "admin" {
services.EnsureLogSourcesForRunningDeployments()
}
list := services.ListVisibleLogSources(c.GetUint("userId"), c.GetString("role") == "admin")
utils.Success(c, utils.PageData{List: list, Total: int64(len(list)), Page: 1, PageSize: 100})
}
func (ctrl *LogsController) CreateSource(c *gin.Context) {
var req dto.LogSourceRequest
if !utils.BindAndValidate(c, &req) {
return
}
if req.Name == "" || req.TargetType == "" {
utils.BadRequest(c, "名称与类型必填")
return
}
if err := services.ValidateSourceAccess(req.Access); err != nil {
utils.BadRequest(c, err.Error())
return
}
src := &models.LogSource{
Name: req.Name, TargetType: req.TargetType,
Namespace: req.Namespace, Service: req.Service, Port: req.Port,
URL: req.URL, AuthType: req.AuthType, BasicUser: req.BasicUser,
BasicPass: req.BasicPass, BearerToken: req.BearerToken, Headers: req.Headers, SkipTLSVerify: req.SkipTLSVerify, IsDefault: req.IsDefault, Access: req.Access,
}
if req.TargetType == "cluster-svc" {
if req.ClusterID > 0 {
id := req.ClusterID
src.ClusterID = &id
} else {
utils.BadRequest(c, "cluster-svc 类型必须选择集群")
return
}
}
created, err := services.CreateLogSource(src)
if err != nil {
utils.Error(c, 400, err.Error())
return
}
utils.Success(c, created.Sanitized())
}
func (ctrl *LogsController) UpdateSource(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(c, "ID 无效")
return
}
var updates map[string]interface{}
if !utils.BindAndValidate(c, &updates) {
return
}
if src := services.GetLogSource(uint(id)); src != nil && src.Managed && updates["isDefault"] == nil {
utils.BadRequest(c, "平台部署的数据源由系统管理,不可编辑")
return
}
if err := services.UpdateLogSource(uint(id), updates); err != nil {
utils.Error(c, 400, err.Error())
return
}
utils.Success(c, gin.H{"message": "updated"})
}
func (ctrl *LogsController) DeleteSource(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.BadRequest(c, "ID 无效")
return
}
if src := services.GetLogSource(uint(id)); src != nil && src.Managed {
utils.BadRequest(c, "平台部署的数据源随部署管理,请先删除对应部署")
return
}
if err := services.DeleteLogSource(uint(id)); err != nil {
utils.Error(c, 400, err.Error())
return
}
utils.Success(c, gin.H{"message": "deleted"})
}
// TestSource 数据源连通性测试(按 id 或表单参数)
func (ctrl *LogsController) TestSource(c *gin.Context) {
var req dto.LogSourceRequest
if !utils.BindAndValidate(c, &req) {
return
}
var src *models.LogSource
if req.ID > 0 {
src = services.GetLogSource(req.ID)
if src == nil {
utils.NotFound(c, "数据源不存在")
return
}
} else {
src = &models.LogSource{
TargetType: req.TargetType, Namespace: req.Namespace, Service: req.Service,
Port: req.Port, URL: req.URL, AuthType: req.AuthType,
BasicUser: req.BasicUser, BasicPass: req.BasicPass, BearerToken: req.BearerToken, Headers: req.Headers, SkipTLSVerify: req.SkipTLSVerify,
}
if req.TargetType == "cluster-svc" && req.ClusterID > 0 {
id := req.ClusterID
src.ClusterID = &id
}
}
utils.Success(c, services.TestLogSource(c.Request.Context(), src))
}
// ==================== 查询代理 ====================
func (ctrl *LogsController) Query(c *gin.Context) {
var req dto.LogQueryRequest
if !utils.BindAndValidate(c, &req) {
return
}
src := services.GetLogSource(req.SourceID)
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
query, err := services.AuthorizedLogQuery(src, c.GetUint("userId"), c.GetString("role") == "admin", req.Query)
if err != nil {
utils.Forbidden(c, err.Error())
return
}
result, err := services.QueryLokiLogs(c.Request.Context(), src, query,
req.Start, req.End, req.Limit, req.Direction, req.Cursor)
if err != nil {
utils.Error(c, 502, err.Error())
return
}
utils.Success(c, result)
}
func (ctrl *LogsController) Labels(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("sourceId"), 10, 64)
src := services.GetLogSource(uint(id))
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
if _, err := services.AuthorizedLogQuery(src, c.GetUint("userId"), c.GetString("role") == "admin", "{}"); err != nil {
utils.Forbidden(c, err.Error())
return
}
start, _ := strconv.ParseInt(c.Query("start"), 10, 64)
end, _ := strconv.ParseInt(c.Query("end"), 10, 64)
labels, err := services.LokiLabels(c.Request.Context(), src, start, end)
if err != nil {
utils.Error(c, 502, err.Error())
return
}
utils.Success(c, labels)
}
func (ctrl *LogsController) LabelValues(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("sourceId"), 10, 64)
src := services.GetLogSource(uint(id))
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
start, _ := strconv.ParseInt(c.Query("start"), 10, 64)
end, _ := strconv.ParseInt(c.Query("end"), 10, 64)
requestedQuery := c.Query("query")
if strings.TrimSpace(requestedQuery) == "" {
requestedQuery = "{}"
}
query, err := services.AuthorizedLogQuery(src, c.GetUint("userId"), c.GetString("role") == "admin", requestedQuery)
if err != nil {
utils.Forbidden(c, err.Error())
return
}
values, err := services.LokiLabelValues(c.Request.Context(), src,
c.Param("name"), query, start, end)
if err != nil {
utils.Error(c, 502, err.Error())
return
}
utils.Success(c, values)
}
// MyScope 返回当前登录用户在某数据源上的授权范围。
//
// 快捷 LogQL 用它做两件事:
// 1. 把授权策略里配置的 namespace 作为默认选中值(管理员配了 default/astralx 后开箱即用)
// 2. 把该值合并进 namespace 候选列表 —— 否则像 default 这种「策略里有、Loki 里没日志」
// 的命名空间不会出现在下拉里,用户会以为策略没生效
func (ctrl *LogsController) MyScope(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("sourceId"), 10, 64)
src := services.GetLogSource(uint(id))
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
if c.GetString("role") == "admin" {
// 管理员的查询范围不受策略限制(unrestricted=true),
// 但仍返回策略里的 labels 作为快捷 LogQL 的默认选中值——
// 否则管理员在小黑框里看到的下拉永远没有策略配的那个 namespace,
// 会误以为「默认策略没生效」。
labels := services.UserAccessLabels(src, c.GetUint("userId"))
if labels == nil {
labels = map[string]string{}
}
utils.Success(c, gin.H{"labels": labels, "unrestricted": true})
return
}
labels := services.UserAccessLabels(src, c.GetUint("userId"))
if labels == nil {
utils.Forbidden(c, "无权查询此数据源")
return
}
utils.Success(c, gin.H{"labels": labels, "unrestricted": len(labels) == 0})
}
// Health Loki /ready 探活(查询页状态点)
func (ctrl *LogsController) Health(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("sourceId"), 10, 64)
src := services.GetLogSource(uint(id))
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
if _, err := services.AuthorizedLogQuery(src, c.GetUint("userId"), c.GetString("role") == "admin", "{}"); err != nil {
utils.Forbidden(c, err.Error())
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
utils.Success(c, gin.H{"ready": services.LokiReady(ctx, src)})
}
func (ctrl *LogsController) Histogram(c *gin.Context) {
var req dto.LogQueryRequest
if !utils.BindAndValidate(c, &req) {
return
}
src := services.GetLogSource(req.SourceID)
if src == nil {
utils.BadRequest(c, "数据源不存在")
return
}
query, err := services.AuthorizedLogQuery(src, c.GetUint("userId"), c.GetString("role") == "admin", req.Query)
if err != nil {
utils.Forbidden(c, err.Error())
return
}
buckets, err := services.LokiHistogram(c.Request.Context(), src, query, req.Start, req.End)
if err != nil {
utils.Error(c, 502, err.Error())
return
}
utils.Success(c, buckets)
}