Files
gotail/backend/main.go
Luiz Costa 3a5cbe12ef feat: initialize web tail pro application with frontend and backend
- 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
2025-11-18 08:27:21 -03:00

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)
}
}