package handlers import ( "bufio" "encoding/json" "fmt" "net/http" "os" "strconv" "strings" "github.com/rs/zerolog/log" "github.com/seu-usuario/go-react-web-tail/internal/broker" "github.com/seu-usuario/go-react-web-tail/internal/models" ) // Handler contém os handlers HTTP da aplicação type Handler struct { broker *broker.Broker logFiles []string } // New cria uma nova instância do Handler func New(broker *broker.Broker, logFiles []string) *Handler { return &Handler{ broker: broker, logFiles: logFiles, } } // HandleSSE gerencia conexões Server-Sent Events para streaming de logs func (h *Handler) HandleSSE(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") clientChan := h.broker.Subscribe() defer h.broker.Unsubscribe(clientChan) log.Info().Str("remote_addr", r.RemoteAddr).Msg("SSE client connected") for { select { case entry := <-clientChan: data, err := json.Marshal(entry) if err != nil { log.Error().Err(err).Msg("Failed to marshal log entry") continue } fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() case <-r.Context().Done(): log.Info().Str("remote_addr", r.RemoteAddr).Msg("SSE client disconnected") return } } } // HandleFiles retorna a lista de arquivos de log monitorados func (h *Handler) HandleFiles(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(h.logFiles); err != nil { log.Error().Err(err).Msg("Failed to encode log files") http.Error(w, "Internal server error", http.StatusInternalServerError) } } // HandleLastLines retorna as últimas N linhas de um arquivo func (h *Handler) HandleLastLines(w http.ResponseWriter, r *http.Request) { file := r.URL.Query().Get("file") nParam := r.URL.Query().Get("n") n := 200 if nParam != "" { if parsed, err := strconv.Atoi(nParam); err == nil && parsed > 0 { n = parsed } } // Validar que o arquivo está na lista permitida allowed := false for _, f := range h.logFiles { if f == file { allowed = true break } } if !allowed { log.Warn().Str("file", file).Str("remote_addr", r.RemoteAddr).Msg("Attempted access to unauthorized file") http.Error(w, "Invalid file", http.StatusBadRequest) return } entries, err := h.getLastLines(file, n) if err != nil { log.Error().Err(err).Str("file", file).Msg("Failed to read last lines") http.Error(w, "Failed to read file", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(entries); err != nil { log.Error().Err(err).Msg("Failed to encode entries") } } // getLastLines lê as últimas N linhas de um arquivo func (h *Handler) getLastLines(filename string, n int) ([]models.LogEntry, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() scanner := bufio.NewScanner(f) buf := make([]string, 0, n) for scanner.Scan() { line := scanner.Text() if len(buf) == n { buf = buf[1:] } buf = append(buf, line) } if err := scanner.Err(); err != nil { return nil, err } entries := make([]models.LogEntry, 0, len(buf)) for _, line := range buf { entry := models.NewLogEntry(filename, strings.TrimRight(line, "\r")) entries = append(entries, entry) } return entries, nil } // HandleHealth retorna o status de saúde da aplicação func (h *Handler) HandleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "healthy", }) } // HandleMetrics retorna métricas da aplicação func (h *Handler) HandleMetrics(w http.ResponseWriter, r *http.Request) { activeClients, totalBroadcasts := h.broker.GetMetrics() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "active_clients": activeClients, "total_broadcasts": totalBroadcasts, "monitored_files": len(h.logFiles), }) }