| package admin |
|
|
| import ( |
| "errors" |
| "fmt" |
| "io" |
| "net/http" |
| "strconv" |
| "strings" |
| "time" |
|
|
| "github.com/Wei-Shaw/sub2api/internal/pkg/response" |
| "github.com/Wei-Shaw/sub2api/internal/server/middleware" |
| "github.com/Wei-Shaw/sub2api/internal/service" |
| "github.com/gin-gonic/gin" |
| ) |
|
|
| type OpsHandler struct { |
| opsService *service.OpsService |
| } |
|
|
| |
| |
| func (h *OpsHandler) GetErrorLogByID(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| detail, err := h.opsService.GetErrorLogByID(c.Request.Context(), id) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| response.Success(c, detail) |
| } |
|
|
| const ( |
| opsListViewErrors = "errors" |
| opsListViewExcluded = "excluded" |
| opsListViewAll = "all" |
| ) |
|
|
| func parseOpsViewParam(c *gin.Context) string { |
| if c == nil { |
| return "" |
| } |
| v := strings.ToLower(strings.TrimSpace(c.Query("view"))) |
| switch v { |
| case "", opsListViewErrors: |
| return opsListViewErrors |
| case opsListViewExcluded: |
| return opsListViewExcluded |
| case opsListViewAll: |
| return opsListViewAll |
| default: |
| return opsListViewErrors |
| } |
| } |
|
|
| func NewOpsHandler(opsService *service.OpsService) *OpsHandler { |
| return &OpsHandler{opsService: opsService} |
| } |
|
|
| |
| |
| func (h *OpsHandler) GetErrorLogs(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| page, pageSize := response.ParsePagination(c) |
| |
| if pageSize > 500 { |
| pageSize = 500 |
| } |
|
|
| startTime, endTime, err := parseOpsTimeRange(c, "1h") |
| if err != nil { |
| response.BadRequest(c, err.Error()) |
| return |
| } |
|
|
| filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize} |
|
|
| if !startTime.IsZero() { |
| filter.StartTime = &startTime |
| } |
| if !endTime.IsZero() { |
| filter.EndTime = &endTime |
| } |
| filter.View = parseOpsViewParam(c) |
| filter.Phase = strings.TrimSpace(c.Query("phase")) |
| filter.Owner = strings.TrimSpace(c.Query("error_owner")) |
| filter.Source = strings.TrimSpace(c.Query("error_source")) |
| filter.Query = strings.TrimSpace(c.Query("q")) |
| filter.UserQuery = strings.TrimSpace(c.Query("user_query")) |
|
|
| |
| |
| if strings.EqualFold(strings.TrimSpace(filter.Phase), "upstream") { |
| filter.Phase = "" |
| } |
|
|
| if platform := strings.TrimSpace(c.Query("platform")); platform != "" { |
| filter.Platform = platform |
| } |
| if v := strings.TrimSpace(c.Query("group_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid group_id") |
| return |
| } |
| filter.GroupID = &id |
| } |
| if v := strings.TrimSpace(c.Query("account_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid account_id") |
| return |
| } |
| filter.AccountID = &id |
| } |
|
|
| if v := strings.TrimSpace(c.Query("resolved")); v != "" { |
| switch strings.ToLower(v) { |
| case "1", "true", "yes": |
| b := true |
| filter.Resolved = &b |
| case "0", "false", "no": |
| b := false |
| filter.Resolved = &b |
| default: |
| response.BadRequest(c, "Invalid resolved") |
| return |
| } |
| } |
| if statusCodesStr := strings.TrimSpace(c.Query("status_codes")); statusCodesStr != "" { |
| parts := strings.Split(statusCodesStr, ",") |
| out := make([]int, 0, len(parts)) |
| for _, part := range parts { |
| p := strings.TrimSpace(part) |
| if p == "" { |
| continue |
| } |
| n, err := strconv.Atoi(p) |
| if err != nil || n < 0 { |
| response.BadRequest(c, "Invalid status_codes") |
| return |
| } |
| out = append(out, n) |
| } |
| filter.StatusCodes = out |
| } |
|
|
| result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize) |
| } |
|
|
| |
| |
| func (h *OpsHandler) ListRequestErrors(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| page, pageSize := response.ParsePagination(c) |
| if pageSize > 500 { |
| pageSize = 500 |
| } |
| startTime, endTime, err := parseOpsTimeRange(c, "1h") |
| if err != nil { |
| response.BadRequest(c, err.Error()) |
| return |
| } |
|
|
| filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize} |
| if !startTime.IsZero() { |
| filter.StartTime = &startTime |
| } |
| if !endTime.IsZero() { |
| filter.EndTime = &endTime |
| } |
| filter.View = parseOpsViewParam(c) |
| filter.Phase = strings.TrimSpace(c.Query("phase")) |
| filter.Owner = strings.TrimSpace(c.Query("error_owner")) |
| filter.Source = strings.TrimSpace(c.Query("error_source")) |
| filter.Query = strings.TrimSpace(c.Query("q")) |
| filter.UserQuery = strings.TrimSpace(c.Query("user_query")) |
|
|
| |
| |
| if strings.EqualFold(strings.TrimSpace(filter.Phase), "upstream") { |
| filter.Phase = "" |
| } |
|
|
| if platform := strings.TrimSpace(c.Query("platform")); platform != "" { |
| filter.Platform = platform |
| } |
| if v := strings.TrimSpace(c.Query("group_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid group_id") |
| return |
| } |
| filter.GroupID = &id |
| } |
| if v := strings.TrimSpace(c.Query("account_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid account_id") |
| return |
| } |
| filter.AccountID = &id |
| } |
|
|
| if v := strings.TrimSpace(c.Query("resolved")); v != "" { |
| switch strings.ToLower(v) { |
| case "1", "true", "yes": |
| b := true |
| filter.Resolved = &b |
| case "0", "false", "no": |
| b := false |
| filter.Resolved = &b |
| default: |
| response.BadRequest(c, "Invalid resolved") |
| return |
| } |
| } |
| if statusCodesStr := strings.TrimSpace(c.Query("status_codes")); statusCodesStr != "" { |
| parts := strings.Split(statusCodesStr, ",") |
| out := make([]int, 0, len(parts)) |
| for _, part := range parts { |
| p := strings.TrimSpace(part) |
| if p == "" { |
| continue |
| } |
| n, err := strconv.Atoi(p) |
| if err != nil || n < 0 { |
| response.BadRequest(c, "Invalid status_codes") |
| return |
| } |
| out = append(out, n) |
| } |
| filter.StatusCodes = out |
| } |
|
|
| result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize) |
| } |
|
|
| |
| |
| func (h *OpsHandler) GetRequestError(c *gin.Context) { |
| |
| h.GetErrorLogByID(c) |
| } |
|
|
| |
| |
| func (h *OpsHandler) ListRequestErrorUpstreamErrors(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| |
| detail, err := h.opsService.GetErrorLogByID(c.Request.Context(), id) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| |
| requestID := strings.TrimSpace(detail.RequestID) |
| clientRequestID := strings.TrimSpace(detail.ClientRequestID) |
| if requestID == "" && clientRequestID == "" { |
| response.Paginated(c, []*service.OpsErrorLog{}, 0, 1, 10) |
| return |
| } |
|
|
| page, pageSize := response.ParsePagination(c) |
| if pageSize > 500 { |
| pageSize = 500 |
| } |
|
|
| |
| |
| startTime, endTime, err := parseOpsTimeRange(c, "30d") |
| if err != nil { |
| response.BadRequest(c, err.Error()) |
| return |
| } |
|
|
| filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize} |
| if !startTime.IsZero() { |
| filter.StartTime = &startTime |
| } |
| if !endTime.IsZero() { |
| filter.EndTime = &endTime |
| } |
| filter.View = "all" |
| filter.Phase = "upstream" |
| filter.Owner = "provider" |
| filter.Source = strings.TrimSpace(c.Query("error_source")) |
| filter.Query = strings.TrimSpace(c.Query("q")) |
|
|
| if platform := strings.TrimSpace(c.Query("platform")); platform != "" { |
| filter.Platform = platform |
| } |
|
|
| |
| if requestID != "" { |
| filter.RequestID = requestID |
| } else { |
| filter.ClientRequestID = clientRequestID |
| } |
|
|
| result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| |
| includeDetail := strings.TrimSpace(c.Query("include_detail")) |
| if includeDetail == "1" || strings.EqualFold(includeDetail, "true") || strings.EqualFold(includeDetail, "yes") { |
| details := make([]*service.OpsErrorLogDetail, 0, len(result.Errors)) |
| for _, item := range result.Errors { |
| if item == nil { |
| continue |
| } |
| d, err := h.opsService.GetErrorLogByID(c.Request.Context(), item.ID) |
| if err != nil || d == nil { |
| continue |
| } |
| details = append(details, d) |
| } |
| response.Paginated(c, details, int64(result.Total), result.Page, result.PageSize) |
| return |
| } |
|
|
| response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize) |
| } |
|
|
| |
| |
| func (h *OpsHandler) RetryRequestErrorClient(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| subject, ok := middleware.GetAuthSubjectFromContext(c) |
| if !ok || subject.UserID <= 0 { |
| response.Error(c, http.StatusUnauthorized, "Unauthorized") |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| result, err := h.opsService.RetryError(c.Request.Context(), subject.UserID, id, service.OpsRetryModeClient, nil) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Success(c, result) |
| } |
|
|
| |
| |
| func (h *OpsHandler) RetryRequestErrorUpstreamEvent(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| subject, ok := middleware.GetAuthSubjectFromContext(c) |
| if !ok || subject.UserID <= 0 { |
| response.Error(c, http.StatusUnauthorized, "Unauthorized") |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| idxStr := strings.TrimSpace(c.Param("idx")) |
| idx, err := strconv.Atoi(idxStr) |
| if err != nil || idx < 0 { |
| response.BadRequest(c, "Invalid upstream idx") |
| return |
| } |
|
|
| result, err := h.opsService.RetryUpstreamEvent(c.Request.Context(), subject.UserID, id, idx) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Success(c, result) |
| } |
|
|
| |
| |
| func (h *OpsHandler) ResolveRequestError(c *gin.Context) { |
| h.UpdateErrorResolution(c) |
| } |
|
|
| |
| |
| func (h *OpsHandler) ListUpstreamErrors(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| page, pageSize := response.ParsePagination(c) |
| if pageSize > 500 { |
| pageSize = 500 |
| } |
| startTime, endTime, err := parseOpsTimeRange(c, "1h") |
| if err != nil { |
| response.BadRequest(c, err.Error()) |
| return |
| } |
|
|
| filter := &service.OpsErrorLogFilter{Page: page, PageSize: pageSize} |
| if !startTime.IsZero() { |
| filter.StartTime = &startTime |
| } |
| if !endTime.IsZero() { |
| filter.EndTime = &endTime |
| } |
|
|
| filter.View = parseOpsViewParam(c) |
| filter.Phase = "upstream" |
| filter.Owner = "provider" |
| filter.Source = strings.TrimSpace(c.Query("error_source")) |
| filter.Query = strings.TrimSpace(c.Query("q")) |
|
|
| if platform := strings.TrimSpace(c.Query("platform")); platform != "" { |
| filter.Platform = platform |
| } |
| if v := strings.TrimSpace(c.Query("group_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid group_id") |
| return |
| } |
| filter.GroupID = &id |
| } |
| if v := strings.TrimSpace(c.Query("account_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid account_id") |
| return |
| } |
| filter.AccountID = &id |
| } |
|
|
| if v := strings.TrimSpace(c.Query("resolved")); v != "" { |
| switch strings.ToLower(v) { |
| case "1", "true", "yes": |
| b := true |
| filter.Resolved = &b |
| case "0", "false", "no": |
| b := false |
| filter.Resolved = &b |
| default: |
| response.BadRequest(c, "Invalid resolved") |
| return |
| } |
| } |
| if statusCodesStr := strings.TrimSpace(c.Query("status_codes")); statusCodesStr != "" { |
| parts := strings.Split(statusCodesStr, ",") |
| out := make([]int, 0, len(parts)) |
| for _, part := range parts { |
| p := strings.TrimSpace(part) |
| if p == "" { |
| continue |
| } |
| n, err := strconv.Atoi(p) |
| if err != nil || n < 0 { |
| response.BadRequest(c, "Invalid status_codes") |
| return |
| } |
| out = append(out, n) |
| } |
| filter.StatusCodes = out |
| } |
|
|
| result, err := h.opsService.GetErrorLogs(c.Request.Context(), filter) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Paginated(c, result.Errors, int64(result.Total), result.Page, result.PageSize) |
| } |
|
|
| |
| |
| func (h *OpsHandler) GetUpstreamError(c *gin.Context) { |
| h.GetErrorLogByID(c) |
| } |
|
|
| |
| |
| func (h *OpsHandler) RetryUpstreamError(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| subject, ok := middleware.GetAuthSubjectFromContext(c) |
| if !ok || subject.UserID <= 0 { |
| response.Error(c, http.StatusUnauthorized, "Unauthorized") |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| result, err := h.opsService.RetryError(c.Request.Context(), subject.UserID, id, service.OpsRetryModeUpstream, nil) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Success(c, result) |
| } |
|
|
| |
| |
| func (h *OpsHandler) ResolveUpstreamError(c *gin.Context) { |
| h.UpdateErrorResolution(c) |
| } |
|
|
| |
|
|
| |
| |
| func (h *OpsHandler) ListRequestDetails(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| page, pageSize := response.ParsePagination(c) |
| if pageSize > 100 { |
| pageSize = 100 |
| } |
|
|
| startTime, endTime, err := parseOpsTimeRange(c, "1h") |
| if err != nil { |
| response.BadRequest(c, err.Error()) |
| return |
| } |
|
|
| filter := &service.OpsRequestDetailFilter{ |
| Page: page, |
| PageSize: pageSize, |
| StartTime: &startTime, |
| EndTime: &endTime, |
| } |
|
|
| filter.Kind = strings.TrimSpace(c.Query("kind")) |
| filter.Platform = strings.TrimSpace(c.Query("platform")) |
| filter.Model = strings.TrimSpace(c.Query("model")) |
| filter.RequestID = strings.TrimSpace(c.Query("request_id")) |
| filter.Query = strings.TrimSpace(c.Query("q")) |
| filter.Sort = strings.TrimSpace(c.Query("sort")) |
|
|
| if v := strings.TrimSpace(c.Query("user_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid user_id") |
| return |
| } |
| filter.UserID = &id |
| } |
| if v := strings.TrimSpace(c.Query("api_key_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid api_key_id") |
| return |
| } |
| filter.APIKeyID = &id |
| } |
| if v := strings.TrimSpace(c.Query("account_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid account_id") |
| return |
| } |
| filter.AccountID = &id |
| } |
| if v := strings.TrimSpace(c.Query("group_id")); v != "" { |
| id, err := strconv.ParseInt(v, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid group_id") |
| return |
| } |
| filter.GroupID = &id |
| } |
|
|
| if v := strings.TrimSpace(c.Query("min_duration_ms")); v != "" { |
| parsed, err := strconv.Atoi(v) |
| if err != nil || parsed < 0 { |
| response.BadRequest(c, "Invalid min_duration_ms") |
| return |
| } |
| filter.MinDurationMs = &parsed |
| } |
| if v := strings.TrimSpace(c.Query("max_duration_ms")); v != "" { |
| parsed, err := strconv.Atoi(v) |
| if err != nil || parsed < 0 { |
| response.BadRequest(c, "Invalid max_duration_ms") |
| return |
| } |
| filter.MaxDurationMs = &parsed |
| } |
|
|
| out, err := h.opsService.ListRequestDetails(c.Request.Context(), filter) |
| if err != nil { |
| |
| if strings.Contains(strings.ToLower(err.Error()), "invalid") { |
| response.BadRequest(c, err.Error()) |
| return |
| } |
| response.Error(c, http.StatusInternalServerError, "Failed to list request details") |
| return |
| } |
|
|
| response.Paginated(c, out.Items, out.Total, out.Page, out.PageSize) |
| } |
|
|
| type opsRetryRequest struct { |
| Mode string `json:"mode"` |
| PinnedAccountID *int64 `json:"pinned_account_id"` |
| Force bool `json:"force"` |
| } |
|
|
| type opsResolveRequest struct { |
| Resolved bool `json:"resolved"` |
| } |
|
|
| |
| |
| func (h *OpsHandler) RetryErrorRequest(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| subject, ok := middleware.GetAuthSubjectFromContext(c) |
| if !ok || subject.UserID <= 0 { |
| response.Error(c, http.StatusUnauthorized, "Unauthorized") |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| req := opsRetryRequest{Mode: service.OpsRetryModeClient} |
| if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { |
| response.BadRequest(c, "Invalid request: "+err.Error()) |
| return |
| } |
| if strings.TrimSpace(req.Mode) == "" { |
| req.Mode = service.OpsRetryModeClient |
| } |
|
|
| |
| _ = req.Force |
|
|
| |
| |
| if strings.EqualFold(strings.TrimSpace(req.Mode), service.OpsRetryModeUpstream) { |
| response.BadRequest(c, "upstream retry is not supported on this endpoint") |
| return |
| } |
|
|
| result, err := h.opsService.RetryError(c.Request.Context(), subject.UserID, id, req.Mode, req.PinnedAccountID) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| response.Success(c, result) |
| } |
|
|
| |
| |
| func (h *OpsHandler) ListRetryAttempts(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| limit := 50 |
| if v := strings.TrimSpace(c.Query("limit")); v != "" { |
| n, err := strconv.Atoi(v) |
| if err != nil || n <= 0 { |
| response.BadRequest(c, "Invalid limit") |
| return |
| } |
| limit = n |
| } |
|
|
| items, err := h.opsService.ListRetryAttemptsByErrorID(c.Request.Context(), id, limit) |
| if err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Success(c, items) |
| } |
|
|
| |
| |
| func (h *OpsHandler) UpdateErrorResolution(c *gin.Context) { |
| if h.opsService == nil { |
| response.Error(c, http.StatusServiceUnavailable, "Ops service not available") |
| return |
| } |
| if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
|
|
| subject, ok := middleware.GetAuthSubjectFromContext(c) |
| if !ok || subject.UserID <= 0 { |
| response.Error(c, http.StatusUnauthorized, "Unauthorized") |
| return |
| } |
|
|
| idStr := strings.TrimSpace(c.Param("id")) |
| id, err := strconv.ParseInt(idStr, 10, 64) |
| if err != nil || id <= 0 { |
| response.BadRequest(c, "Invalid error id") |
| return |
| } |
|
|
| var req opsResolveRequest |
| if err := c.ShouldBindJSON(&req); err != nil { |
| response.BadRequest(c, "Invalid request: "+err.Error()) |
| return |
| } |
| uid := subject.UserID |
| if err := h.opsService.UpdateErrorResolution(c.Request.Context(), id, req.Resolved, &uid, nil); err != nil { |
| response.ErrorFrom(c, err) |
| return |
| } |
| response.Success(c, gin.H{"ok": true}) |
| } |
|
|
| func parseOpsTimeRange(c *gin.Context, defaultRange string) (time.Time, time.Time, error) { |
| startStr := strings.TrimSpace(c.Query("start_time")) |
| endStr := strings.TrimSpace(c.Query("end_time")) |
|
|
| parseTS := func(s string) (time.Time, error) { |
| if s == "" { |
| return time.Time{}, nil |
| } |
| if t, err := time.Parse(time.RFC3339Nano, s); err == nil { |
| return t, nil |
| } |
| return time.Parse(time.RFC3339, s) |
| } |
|
|
| start, err := parseTS(startStr) |
| if err != nil { |
| return time.Time{}, time.Time{}, err |
| } |
| end, err := parseTS(endStr) |
| if err != nil { |
| return time.Time{}, time.Time{}, err |
| } |
|
|
| |
| if startStr != "" || endStr != "" { |
| if end.IsZero() { |
| end = time.Now() |
| } |
| if start.IsZero() { |
| dur, _ := parseOpsDuration(defaultRange) |
| start = end.Add(-dur) |
| } |
| if start.After(end) { |
| return time.Time{}, time.Time{}, fmt.Errorf("invalid time range: start_time must be <= end_time") |
| } |
| if end.Sub(start) > 30*24*time.Hour { |
| return time.Time{}, time.Time{}, fmt.Errorf("invalid time range: max window is 30 days") |
| } |
| return start, end, nil |
| } |
|
|
| |
| tr := strings.TrimSpace(c.Query("time_range")) |
| if tr == "" { |
| tr = defaultRange |
| } |
| dur, ok := parseOpsDuration(tr) |
| if !ok { |
| dur, _ = parseOpsDuration(defaultRange) |
| } |
|
|
| end = time.Now() |
| start = end.Add(-dur) |
| if end.Sub(start) > 30*24*time.Hour { |
| return time.Time{}, time.Time{}, fmt.Errorf("invalid time range: max window is 30 days") |
| } |
| return start, end, nil |
| } |
|
|
| func parseOpsDuration(v string) (time.Duration, bool) { |
| switch strings.TrimSpace(v) { |
| case "5m": |
| return 5 * time.Minute, true |
| case "30m": |
| return 30 * time.Minute, true |
| case "1h": |
| return time.Hour, true |
| case "6h": |
| return 6 * time.Hour, true |
| case "24h": |
| return 24 * time.Hour, true |
| case "7d": |
| return 7 * 24 * time.Hour, true |
| case "30d": |
| return 30 * 24 * time.Hour, true |
| default: |
| return 0, false |
| } |
| } |
|
|