package handlers import ( "encoding/csv" "encoding/json" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/yourusername/victorialogs-manager/internal/models" "github.com/yourusername/victorialogs-manager/internal/services" ) // LogsHandler handles log querying endpoints type LogsHandler struct { logsService *services.LogsService } // NewLogsHandler creates a new logs handler func NewLogsHandler(logsService *services.LogsService) *LogsHandler { return &LogsHandler{ logsService: logsService, } } // QueryLogs handles log queries // @Summary Query logs // @Description Execute a LogsQL query // @Tags logs // @Accept json // @Produce json // @Param request body services.QueryLogsRequest true "Query parameters" // @Success 200 {object} services.QueryLogsResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Security BearerAuth // @Router /api/v1/logs/query [post] func (h *LogsHandler) QueryLogs(c *gin.Context) { var req services.QueryLogsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "invalid request", Message: err.Error(), }) return } // Get user role from context roleInterface, _ := c.Get("role") userRole := roleInterface.(models.Role) resp, err := h.logsService.QueryLogs(c.Request.Context(), &req, userRole) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "query failed", Message: err.Error(), }) return } c.JSON(http.StatusOK, resp) } // StatsQuery handles stats queries // @Summary Get log statistics // @Description Execute a stats query for time-series data // @Tags logs // @Accept json // @Produce json // @Param request body services.StatsQueryRequest true "Stats query parameters" // @Success 200 {object} services.StatsQueryResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Security BearerAuth // @Router /api/v1/logs/stats [post] func (h *LogsHandler) StatsQuery(c *gin.Context) { var req services.StatsQueryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "invalid request", Message: err.Error(), }) return } resp, err := h.logsService.StatsQuery(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "stats query failed", Message: err.Error(), }) return } c.JSON(http.StatusOK, resp) } // GetFacets handles facets requests // @Summary Get field facets // @Description Get facet values for log fields // @Tags logs // @Accept json // @Produce json // @Param request body services.GetFacetsRequest true "Facets parameters" // @Success 200 {object} services.GetFacetsResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Security BearerAuth // @Router /api/v1/logs/facets [post] func (h *LogsHandler) GetFacets(c *gin.Context) { var req services.GetFacetsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "invalid request", Message: err.Error(), }) return } resp, err := h.logsService.GetFacets(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "facets query failed", Message: err.Error(), }) return } c.JSON(http.StatusOK, resp) } // TailLogs handles WebSocket log tailing // @Summary Tail logs in real-time // @Description Stream logs in real-time via WebSocket // @Tags logs // @Accept json // @Produce json // @Param request body services.TailLogsRequest true "Tail parameters" // @Success 200 {object} SuccessResponse // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Security BearerAuth // @Router /api/v1/logs/tail [post] func (h *LogsHandler) TailLogs(c *gin.Context) { var req services.TailLogsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "invalid request", Message: err.Error(), }) return } // Start tailing logChan, errChan, err := h.logsService.TailLogs(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "tail failed", Message: err.Error(), }) return } // Set headers for streaming c.Header("Content-Type", "application/x-ndjson") c.Header("Transfer-Encoding", "chunked") c.Header("X-Content-Type-Options", "nosniff") // Stream logs as NDJSON c.Stream(func(w gin.ResponseWriter) bool { select { case log, ok := <-logChan: if !ok { return false } data, _ := json.Marshal(log) w.Write(data) w.Write([]byte("\n")) w.Flush() return true case err := <-errChan: if err != nil { errData, _ := json.Marshal(ErrorResponse{ Error: "stream error", Message: err.Error(), }) w.Write(errData) w.Write([]byte("\n")) w.Flush() } return false case <-c.Request.Context().Done(): return false } }) } // ExportLogs handles log export // @Summary Export logs // @Description Export logs in JSON or CSV format // @Tags logs // @Accept json // @Produce json,text/csv // @Param request body services.ExportLogsRequest true "Export parameters" // @Success 200 {file} file // @Failure 400 {object} ErrorResponse // @Failure 401 {object} ErrorResponse // @Security BearerAuth // @Router /api/v1/logs/export [post] func (h *LogsHandler) ExportLogs(c *gin.Context) { var req services.ExportLogsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "invalid request", Message: err.Error(), }) return } // Default format to JSON if req.Format == "" { req.Format = "json" } // Get user role from context roleInterface, _ := c.Get("role") userRole := roleInterface.(models.Role) logs, err := h.logsService.ExportLogs(c.Request.Context(), &req, userRole) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "export failed", Message: err.Error(), }) return } // Export based on format switch req.Format { case "csv": h.exportCSV(c, logs) case "json": h.exportJSON(c, logs) default: c.JSON(http.StatusBadRequest, ErrorResponse{ Error: "invalid format", Message: "format must be 'json' or 'csv'", }) } } // exportJSON exports logs as JSON func (h *LogsHandler) exportJSON(c *gin.Context, logs []interface{}) { filename := "logs_" + strconv.FormatInt(c.Request.Context().Value("timestamp").(int64), 10) + ".json" c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Type", "application/json") c.JSON(http.StatusOK, gin.H{ "logs": logs, "count": len(logs), }) } // exportCSV exports logs as CSV func (h *LogsHandler) exportCSV(c *gin.Context, logs []interface{}) { filename := "logs_" + strconv.FormatInt(c.Request.Context().Value("timestamp").(int64), 10) + ".csv" c.Header("Content-Disposition", "attachment; filename="+filename) c.Header("Content-Type", "text/csv") writer := csv.NewWriter(c.Writer) defer writer.Flush() // Write CSV header writer.Write([]string{"timestamp", "message", "stream_id", "fields"}) // Write logs for _, log := range logs { logMap, ok := log.(map[string]interface{}) if !ok { continue } timestamp := "" if t, ok := logMap["_time"]; ok { timestamp = t.(string) } message := "" if m, ok := logMap["_msg"]; ok { message = m.(string) } streamID := "" if s, ok := logMap["_stream_id"]; ok { streamID = s.(string) } fields := "" if f, ok := logMap["fields"]; ok { fieldsJSON, _ := json.Marshal(f) fields = string(fieldsJSON) } writer.Write([]string{timestamp, message, streamID, fields}) } }