- Add frontend React application with log viewing components - Implement backend Go server for log tailing and SSE streaming - Include build scripts for both frontend and backend - Set up basic authentication and file handling - Add styling and log formatting capabilities
207 lines
5.7 KiB
Go
207 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"embed"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"sync"
|
|
|
|
"github.com/hpcloud/tail"
|
|
)
|
|
|
|
//go:embed ../frontend/dist/*
|
|
var staticFiles embed.FS
|
|
|
|
// Variáveis para os argumentos de linha de comando
|
|
var (
|
|
port string
|
|
password string
|
|
)
|
|
|
|
// init() é executado antes de main(). É o lugar ideal para configurar flags.
|
|
func init() {
|
|
flag.StringVar(&port, "port", ":8080", "Porta para o servidor web (ex: :8080, 9090)")
|
|
flag.StringVar(&password, "password", "", "Senha para autenticar o acesso à interface web")
|
|
}
|
|
|
|
// LogEntry representa uma linha de log
|
|
type LogEntry struct {
|
|
Filename string `json:"filename"`
|
|
Line string `json:"line"`
|
|
Timestamp string `json:"timestamp"`
|
|
}
|
|
|
|
// Broker gerencia os clientes SSE e broadcasting
|
|
type Broker struct {
|
|
clients map[chan LogEntry]bool
|
|
mu sync.Mutex
|
|
}
|
|
|
|
var broker = &Broker{
|
|
clients: make(map[chan LogEntry]bool),
|
|
}
|
|
|
|
func (b *Broker) Subscribe() chan LogEntry {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
ch := make(chan LogEntry, 100)
|
|
b.clients[ch] = true
|
|
return ch
|
|
}
|
|
|
|
func (b *Broker) Unsubscribe(ch chan LogEntry) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if _, ok := b.clients[ch]; ok {
|
|
delete(b.clients, ch)
|
|
close(ch)
|
|
}
|
|
}
|
|
|
|
func (b *Broker) Broadcast(entry LogEntry) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
for clientCh := range b.clients {
|
|
select {
|
|
case clientCh <- entry:
|
|
default:
|
|
delete(b.clients, clientCh)
|
|
close(clientCh)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleSSE envia logs em tempo real via SSE
|
|
func 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")
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
clientChan := broker.Subscribe()
|
|
defer broker.Unsubscribe(clientChan)
|
|
|
|
for {
|
|
select {
|
|
case entry := <-clientChan:
|
|
data, _ := json.Marshal(entry)
|
|
fmt.Fprintf(w, "data: %s\n\n", data)
|
|
flusher.Flush()
|
|
case <-r.Context().Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleFiles lista os arquivos de log disponíveis
|
|
func handleFiles(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(os.Args[1:])
|
|
}
|
|
|
|
// startTailing inicia o monitoramento dos arquivos
|
|
func startTailing(filenames []string) {
|
|
for _, filename := range filenames {
|
|
tailConfig := tail.Config{
|
|
Follow: true,
|
|
ReOpen: true,
|
|
Location: &tail.SeekInfo{Offset: 0, Whence: os.SEEK_END},
|
|
}
|
|
t, err := tail.TailFile(filename, tailConfig)
|
|
if err != nil {
|
|
log.Printf("Erro ao fazer tail do arquivo %s: %v", filename, err)
|
|
continue
|
|
}
|
|
log.Printf("Iniciando tail no arquivo: %s", filename)
|
|
go func(t *tail.Tail, fn string) {
|
|
for line := range t.Lines {
|
|
if line.Err != nil {
|
|
log.Printf("Erro lendo linha de %s: %v", fn, line.Err)
|
|
continue
|
|
}
|
|
timestamp := ""
|
|
if match := regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`).FindString(line.Text); match != "" {
|
|
timestamp = match
|
|
} else if match := regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}`).FindString(line.Text); match != "" {
|
|
timestamp = match
|
|
}
|
|
broker.Broadcast(LogEntry{Filename: fn, Line: line.Text, Timestamp: timestamp})
|
|
}
|
|
}(t, filename)
|
|
}
|
|
}
|
|
|
|
// authMiddleware é um middleware que protege as rotas com HTTP Basic Auth
|
|
func authMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if password == "" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
user, pass, ok := r.BasicAuth()
|
|
if !ok {
|
|
requestAuth(w)
|
|
return
|
|
}
|
|
usernameMatch := subtle.ConstantTimeCompare([]byte(user), []byte("admin")) == 1
|
|
passwordMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(password)) == 1
|
|
if usernameMatch && passwordMatch {
|
|
next.ServeHTTP(w, r)
|
|
} else {
|
|
requestAuth(w)
|
|
}
|
|
})
|
|
}
|
|
|
|
// requestAuth envia o header 401 Unauthorized
|
|
func requestAuth(w http.ResponseWriter) {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Web Tail Pro - Acesso Restrito"`)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte("Não autorizado.\n"))
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
logFiles := flag.Args()
|
|
if len(logFiles) < 1 {
|
|
fmt.Println("Uso: web-tail-pro [opções] <arquivo1.log> <arquivo2.log> ...")
|
|
fmt.Println("\nOpções:")
|
|
flag.PrintDefaults()
|
|
os.Exit(1)
|
|
}
|
|
|
|
startTailing(logFiles)
|
|
|
|
distFS, err := fs.Sub(staticFiles, "frontend/dist")
|
|
if err != nil {
|
|
log.Fatal("Erro ao criar sub-filesystem para arquivos estáticos:", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/", http.FileServer(http.FS(distFS)))
|
|
mux.HandleFunc("/logs", handleSSE)
|
|
mux.HandleFunc("/api/files", handleFiles)
|
|
|
|
protectedHandler := authMiddleware(mux)
|
|
|
|
log.Printf("Servidor iniciado em http://localhost%s", port)
|
|
if password != "" {
|
|
log.Println("🔐 Autenticação por senha está ATIVADA.")
|
|
}
|
|
if err := http.ListenAndServe(port, protectedHandler); err != nil {
|
|
log.Fatal("Erro ao iniciar o servidor:", err)
|
|
}
|
|
}
|