Files
HellreigN/backend/internal/handlers/logs.go
T
d3m0k1d d96f952d73
ci-agent / build (push) Failing after 22s
chore: add clickhouse as db for logs on agent and search
2026-04-03 23:23:43 +03:00

230 lines
5.8 KiB
Go

package handlers
import (
"context"
"net/http"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
"github.com/gin-gonic/gin"
)
type LogHandlers struct {
LogRepo *repository.LogRepository
}
func NewLogHandlers(logRepo *repository.LogRepository) *LogHandlers {
return &LogHandlers{LogRepo: logRepo}
}
type InsertLogRequest struct {
Timestamp time.Time `json:"timestamp"`
Level string `json:"level" binding:"required"`
Service string `json:"service" binding:"required"`
Agent string `json:"agent" binding:"required"`
Message string `json:"message" binding:"required"`
}
// @Summary Insert log entry
// @Description Inserts a single log entry into ClickHouse
// @Tags logs
// @Accept json
// @Produce json
// @Param body body InsertLogRequest true "Log entry"
// @Success 201 {object} map[string]string
// @Router /logs [post]
func (lh *LogHandlers) Insert(c *gin.Context) {
var req InsertLogRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Timestamp.IsZero() {
req.Timestamp = time.Now()
}
log := storage.LogEntry{
Timestamp: req.Timestamp,
Level: req.Level,
Service: req.Service,
Agent: req.Agent,
Message: req.Message,
}
if err := lh.LogRepo.Insert(c.Request.Context(), log); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert log"})
return
}
c.JSON(http.StatusCreated, gin.H{"status": "ok"})
}
type InsertLogsRequest struct {
Logs []InsertLogRequest `json:"logs" binding:"required"`
}
// @Summary Insert log entries (batch)
// @Description Inserts multiple log entries into ClickHouse
// @Tags logs
// @Accept json
// @Produce json
// @Param body body InsertLogsRequest true "Log entries"
// @Success 201 {object} map[string]string
// @Router /logs/batch [post]
func (lh *LogHandlers) InsertBatch(c *gin.Context) {
var req InsertLogsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logs := make([]storage.LogEntry, len(req.Logs))
for i, l := range req.Logs {
if l.Timestamp.IsZero() {
l.Timestamp = time.Now()
}
logs[i] = storage.LogEntry{
Timestamp: l.Timestamp,
Level: l.Level,
Service: l.Service,
Agent: l.Agent,
Message: l.Message,
}
}
if err := lh.LogRepo.InsertBatch(c.Request.Context(), logs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert logs"})
return
}
c.JSON(http.StatusCreated, gin.H{"status": "ok", "count": len(logs)})
}
type SearchLogsRequest struct {
Level string `form:"level"`
Service string `form:"service"`
Agent string `form:"agent"`
DateFrom string `form:"date_from"`
DateTo string `form:"date_to"`
Limit int `form:"limit"`
Offset int `form:"offset"`
}
// @Summary Search logs
// @Description Searches logs with various filters
// @Tags logs
// @Produce json
// @Param level query string false "Log level (INFO, WARNING, ERROR, FATAL)"
// @Param service query string false "Service name"
// @Param agent query string false "Agent name"
// @Param date_from query string false "Date from (RFC3339)"
// @Param date_to query string false "Date to (RFC3339)"
// @Param limit query int false "Limit results" default(100)
// @Param offset query int false "Offset results" default(0)
// @Success 200 {array} storage.LogEntry
// @Router /logs [get]
func (lh *LogHandlers) Search(c *gin.Context) {
var req SearchLogsRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
filter := repository.LogFilter{
Level: req.Level,
Service: req.Service,
Agent: req.Agent,
Limit: req.Limit,
Offset: req.Offset,
}
if req.DateFrom != "" {
if t, err := time.Parse(time.RFC3339, req.DateFrom); err == nil {
filter.DateFrom = t
}
}
if req.DateTo != "" {
if t, err := time.Parse(time.RFC3339, req.DateTo); err == nil {
filter.DateTo = t
}
}
if filter.Limit <= 0 {
filter.Limit = 100
}
logs, err := lh.LogRepo.Search(c.Request.Context(), filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search logs"})
return
}
c.JSON(http.StatusOK, logs)
}
// @Summary Get distinct services
// @Description Returns list of all unique service names in logs
// @Tags logs
// @Produce json
// @Success 200 {array} string
// @Router /logs/services [get]
func (lh *LogHandlers) GetServices(c *gin.Context) {
services, err := lh.LogRepo.GetDistinctServices(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get services"})
return
}
if services == nil {
services = []string{}
}
c.JSON(http.StatusOK, services)
}
// @Summary Get distinct agents
// @Description Returns list of all unique agent names in logs
// @Tags logs
// @Produce json
// @Success 200 {array} string
// @Router /logs/agents [get]
func (lh *LogHandlers) GetAgents(c *gin.Context) {
agents, err := lh.LogRepo.GetDistinctAgents(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get agents"})
return
}
if agents == nil {
agents = []string{}
}
c.JSON(http.StatusOK, agents)
}
// @Summary Get distinct log levels
// @Description Returns list of all unique log levels in logs
// @Tags logs
// @Produce json
// @Success 200 {array} string
// @Router /logs/levels [get]
func (lh *LogHandlers) GetLevels(c *gin.Context) {
levels, err := lh.LogRepo.GetDistinctLevels(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get levels"})
return
}
if levels == nil {
levels = []string{}
}
c.JSON(http.StatusOK, levels)
}
// Ensure context is used
var _ = context.Background