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
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
module github.com/seu-usuario/go-react-web-tail
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/tomnomnom/linkheader v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
)
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user