- Create LogsHandler with all log query endpoints - Implement QueryLogs for LogsQL queries - Implement StatsQuery for time-series statistics - Implement GetFacets for field facets - Implement TailLogs for real-time streaming (NDJSON) - Implement ExportLogs with JSON and CSV support - Wire up logs handler in router - Add VictoriaLogs client initialization Closes #13 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
309 lines
7.6 KiB
Go
309 lines
7.6 KiB
Go
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})
|
|
}
|
|
}
|