package controllers import ( "context" "errors" "net/http" "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 { switch { case errors.Is(err, services.ErrTooManyQueries): // 429:并发重查询超限,让前端提示"稍后重试",而不是当成服务故障 utils.Error(c, http.StatusTooManyRequests, err.Error()) case strings.Contains(err.Error(), "统计范围过大"): utils.Error(c, http.StatusBadRequest, err.Error()) default: utils.Error(c, 502, err.Error()) } return } utils.Success(c, buckets) } // Metrics 日志分析指标查询(仅 admin,路由层 AdminAuth 拦截) func (ctrl *LogsController) Metrics(c *gin.Context) { var req dto.LogMetricQueryRequest 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 } series, err := services.LokiMetrics(c.Request.Context(), src, query, req.Start, req.End) if err != nil { utils.Error(c, 502, err.Error()) return } utils.Success(c, series) }