added weblogger to display logs, timestamps and levels on the webpage

This commit is contained in:
Stefan Etringer 2025-04-29 15:03:49 +00:00
parent 0498013d1d
commit 6ea4d31109
10 changed files with 233 additions and 51 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ build/
build/*
pkg/*
agents/agents
logs.db

View File

@ -11,6 +11,7 @@ import (
"math/rand"
"math"
"strconv"
// "gontrol/src/logger"
"github.com/gorilla/websocket"
)
@ -57,6 +58,7 @@ func registerAgent(agentName, agentId, agentIp, agentType string) error {
}
log.Printf("Agent %s successfully registered.", agentName)
logger.LogEntries = append(logger.LogEntries, fmt.Sprintf("%s Agent successfully registered.", time.Now().Format(time.RFC3339)))
return nil
}
@ -67,6 +69,7 @@ func connectToWebSocket(agentName, agentId, agentIp, agentType string) error {
conn, _, err = websocket.DefaultDialer.Dial(wsURL, nil)
if err == nil {
log.Println("WeSocket connection established")
// logger.LogEntries = append(logger.LogEntries, fmt.Sprintf("%s websocket established", time.Now().Format(time.RFC3339)))
return nil
}

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/go-sql-driver/mysql v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/kelseyhightower/envconfig v1.4.0
github.com/mattn/go-sqlite3 v1.14.28
)
require (

2
go.sum
View File

@ -11,6 +11,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

48
main.go
View File

@ -16,6 +16,7 @@ import (
"gontrol/src/randomname"
"gontrol/src/server/database"
"gontrol/src/logger"
api "gontrol/src/server/api"
websocketserver "gontrol/src/server/websocket"
@ -25,8 +26,10 @@ import (
"github.com/kelseyhightower/envconfig"
)
var tmpl *template.Template
var db *sql.DB
var (
tmpl *template.Template
db *sql.DB
)
type Config struct {
Database struct {
@ -135,6 +138,24 @@ func getAgentIds(w http.ResponseWriter, r *http.Request) {
return
}
func logsHandler(w http.ResponseWriter, r *http.Request) {
logs, err := logger.FetchLogs(10)
if err != nil {
http.Error(w, "Error fetching logs", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "<div>")
// for _, logEntry := range logsToSend {
for _, logEntry := range logs {
fmt.Fprintf(w, "<p><strong>[%s] [%s]</strong> %s</p>", logEntry.Timestamp, logEntry.Level, logEntry.Message)
}
fmt.Fprintf(w, "</div>")
}
func main() {
var cfg Config
@ -153,8 +174,16 @@ func main() {
webMux.HandleFunc("/agentNames", getAgentNames)
webMux.HandleFunc("/agentIds", getAgentIds)
webMux.HandleFunc("/agents/{agentId}", agentsHandler)
webMux.HandleFunc("/logs", logsHandler)
// Sqlite3
err := logger.InitDB("/tmp/gontrol_logs.db")
if err != nil {
log.Fatal(err)
}
defer logger.CloseDB()
// initDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
// mysql
db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
defer db.Close()
name := randomname.GenerateRandomName()
@ -168,20 +197,31 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Websocket server is running on port 5555")
logLine := "Websocket server is running on port 5555"
log.Println(logLine)
if err := websocketServer.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Websocket server failed: %s", err)
}
err = logger.InsertLog(logger.Info, logLine)
if err != nil {
log.Println("Error inserting log:", err)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Web server is running on port 3333")
logLine := "Web server is running on port 3333"
log.Println(logLine)
if err := webServer.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Web server failed: %s", err)
}
err = logger.InsertLog(logger.Info, logLine)
if err != nil {
log.Println("Error inserting log:", err)
}
}()
shutdownCh := make(chan os.Signal, 1)

127
src/logger/logger.go Normal file
View File

@ -0,0 +1,127 @@
package logger
import (
"database/sql"
"fmt"
"log"
// "net/http"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
var (
Lite_db *sql.DB
lite_dbMutex sync.Mutex
logMutex sync.Mutex
logLimit = 10
// LogEntries []string
)
const (
Debug LogLevel = "DEBUG"
Info LogLevel = "INFO"
Warning LogLevel = "WARNING"
Error LogLevel = "ERROR"
Fatal LogLevel = "FATAL"
)
type LogLevel string
type LogEntry struct {
Message string
Timestamp string
Level LogLevel
}
func ToLog(logLine string) string {
log := fmt.Sprintf("%s",time.Now().Format(time.RFC3339) + " " + logLine)
return log
}
func InitDB(dbPath string) error {
var err error
Lite_db, err = sql.Open("sqlite3", dbPath)
if err != nil {
return fmt.Errorf("Error opening DB: %w", err)
}
CreateTableQuery := `CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
level TEXT
);`
_, err = Lite_db.Exec(CreateTableQuery)
if err != nil {
return fmt.Errorf("Error creating table: %w", err)
}
return nil
}
func InsertLog(level LogLevel, message string) error {
lite_dbMutex.Lock()
defer lite_dbMutex.Unlock()
// Future use may fulfill multiple transactions, a transaction is used
tx, err := Lite_db.Begin()
if err != nil {
return fmt.Errorf("Error starting transaction: %w", err)
}
// insertQuery := `INSERT INTO logs (message) VALUES (?)`
insertQuery := `INSERT INTO logs (message, level) VALUES (?, ?)`
// _, err := Lite_db.Exec(insertQuery, message, level)
_, err = tx.Exec(insertQuery, message, level)
if err != nil {
tx.Rollback()
return fmt.Errorf("Error inserting log: %v", err)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("Error committing transaction: %w", err)
}
return nil
}
func FetchLogs(limit int) ([]LogEntry, error) {
lite_dbMutex.Lock()
defer lite_dbMutex.Unlock()
query := `SELECT timestamp, level, message FROM logs ORDER BY timestamp DESC LIMIT ?`
rows, err := Lite_db.Query(query, limit)
if err != nil {
return nil, fmt.Errorf("Error fetching logs: %w", err)
}
defer rows.Close()
// var logs []string
var logs []LogEntry
for rows.Next() {
// var message string
var logEntry LogEntry
if err := rows.Scan( &logEntry.Timestamp, &logEntry.Level, &logEntry.Message); err != nil {
return nil, fmt.Errorf("Error scanning row: %w", err)
}
logs = append(logs, logEntry)
}
return logs, nil
}
func CloseDB() {
if Lite_db != nil {
err := Lite_db.Close()
if err != nil {
log.Printf("Error closing database: %v", err)
}
}
}

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"strconv"
"log"
// "gontrol/src/logger"
_ "github.com/go-sql-driver/mysql"
)

View File

@ -3,6 +3,8 @@ package websocketserver
import (
"database/sql"
"encoding/json"
"fmt"
"gontrol/src/logger"
"gontrol/src/randomname"
"gontrol/src/server/api"
"io"
@ -63,7 +65,10 @@ func registerAgent(agentName, agentId, agentIp, agentType string) error {
defer resp.Body.Close()
if resp.StatusCode == http.StatusCreated {
log.Printf("Agent %s successfully registered.", agentName)
logLine := fmt.Sprintf("Agent %s successfully registered.", agentName)
log.Printf(logLine)
// logLine = logger.ToLog(logLine)
logger.InsertLog(logger.Info, logLine)
return nil
} else if resp.StatusCode == http.StatusOK {
log.Printf("Agent %s already registered.", agentName)
@ -81,14 +86,14 @@ func getAgentDetails(agentId string) (*api.Agent, error) {
// var ids []string
resp, err := http.Get(agentURL)
if err != nil {
log.Printf("Failed to make GET request: %w", err)
log.Printf("Failed to make GET request: %s", err)
return nil, err
}
defer resp.Body.Close()
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Printf("Failed to parse HTML: %w", err)
log.Printf("Failed to parse HTML: %s", err)
return nil, err
}
@ -98,7 +103,7 @@ func getAgentDetails(agentId string) (*api.Agent, error) {
if strings.HasPrefix(text, "ID:") {
agent.AgentID, err = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(text, "ID:")))
if err != nil {
log.Printf("Converting string to integer failed in getAgentDetails(): %w", err)
log.Printf("Converting string to integer failed in getAgentDetails(): %s", err)
}
} else if strings.HasPrefix(text, "Name:") {
agent.AgentName = strings.TrimSpace(strings.TrimPrefix(text, "Name:"))
@ -124,7 +129,7 @@ func getAgentIds() ([]string, error) {
// var ids []string
resp, err := http.Get(idURL)
if err != nil {
log.Printf("Failed to make GET request: %w", err)
log.Printf("Failed to make GET request: %s", err)
return nil, err
}
defer resp.Body.Close()
@ -136,13 +141,13 @@ func getAgentIds() ([]string, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Failed to read response body: %w", err)
log.Printf("Failed to read response body: %s", err)
return nil, err
}
var agentIds []string
if err := json.Unmarshal(body, &agentIds); err != nil {
log.Printf("Failed to parse JSON response: %w", err)
log.Printf("Failed to parse JSON response: %s", err)
return nil, err
}
@ -182,7 +187,9 @@ func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
return
}
log.Printf("Agent %s connected: %s (%s)", agentId, agentName, agentIP)
logLine := fmt.Sprintf("Agent %s connected: %s (%s)", agentId, agentName, agentIP)
log.Printf(logLine)
logger.InsertLog(logger.Info, logLine)
agentSocketsMutex.Lock()
agentSockets[agentName] = c
@ -193,17 +200,22 @@ func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
delete(agentSockets, agentName)
agentSocketsMutex.Unlock()
c.Close()
log.Printf("Agent disconnected: %s (%s)", agentName, agentIP)
logLine := fmt.Sprintf("Agent disconnected: %s (%s)", agentName, agentIP)
log.Printf(logLine)
logger.InsertLog(logger.Info, logLine)
}()
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Printf("Error reading from agent %s: %v", agentName, err)
logLine := fmt.Sprintf("Error reading from agent %s: %v", agentName, err)
log.Printf(logLine)
logger.InsertLog(logger.Error, logLine)
break
}
log.Printf("Message from agent %s: %s", agentName, message)
logLine := fmt.Sprintf("Message from agent %s: %s", agentName, message)
log.Printf(logLine)
logger.InsertLog(logger.Debug, logLine)
if ch, ok := responseChannels.Load(agentName); ok {
responseChan := ch.(chan string)
@ -274,8 +286,6 @@ var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Reques
}
func Server() (*http.Server) {
webSocketHandler := webSocketHandler {
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
@ -286,7 +296,7 @@ func Server() (*http.Server) {
corsMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") // Allow the WebUI origin
w.Header().Set("Access-Control-Allow-Origin", "*") // Allow the WebUI origin, this needs to be changed before prod
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, HX-Current-URL, HX-Request, HX-Target, HX-Trigger, HX-Trigger-Name")
if r.Method == "OPTIONS" {

View File

@ -10,6 +10,7 @@
<!-- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> -->
<title>g2: gommand & gontrol</title>
<script>
// Query Agents for the Dropdown Menu
document.addEventListener('DOMContentLoaded', () => {
fetch('/agentNames')
.then(response => response.json())
@ -23,43 +24,19 @@
});
})
.catch(error => console.error('Error fetching agent names:', error));
fetch('http://localhost:5555/agentNames')
.then(response => {
console.log('Fetch response status:', response.status);
return response.json();
})
.then(agentNames => {
console.log('Connected agent names:', agentNames);
// Your existing logic here
})
.catch(error => {
console.error('Error fetching agent names:', error);
});
const tableRows = document.querySelectorAll('#agentList table tbody tr');
console.log('Number of rows found:', tableRows.length);
tableRows.forEach((row, index) => {
const nameCell = row.querySelector('td[data-column="Name"]');
const statusCell = row.querySelector('td[data-column="Status"]');
console.log(`Row ${index + 1} - Name: ${nameCell?.textContent}, Status Cell Found: ${!!statusCell}`);
});
});
// Query agents currently connected to the websocket and put status into the table
const updateAgentStatuses = () => {
fetch('http://localhost:5555/agentNames')
.then(response => response.json())
.then(agentNames => {
console.log("Agent names fetched:", agentNames);
// Get all rows in the table body
const tableRows = document.querySelectorAll('#agentList table tbody tr');
// Loop through each row to find the Name and update Status
tableRows.forEach(row => {
const nameCell = row.querySelector('td:nth-child(2)'); // Name is in the second column
const statusCell = row.querySelector('td:nth-child(5)'); // Status is in the fifth column
const nameCell = row.querySelector('td:nth-child(2)');
const statusCell = row.querySelector('td:nth-child(5)');
if (nameCell && statusCell) {
const agentName = nameCell.textContent.trim();
if (agentNames.includes(agentName)) {
statusCell.innerHTML = '<span class="badge bg-success">Connected</span>';
} else {
@ -70,12 +47,8 @@
})
.catch(error => console.error('Error fetching agent names:', error));
};
// Initial call to update statuses
updateAgentStatuses();
// Set the function to run every 5 seconds
setInterval(updateAgentStatuses, 5000);
setInterval(updateAgentStatuses, 1000);
</script>
</head>
<body>

24
templates/logs.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Server Logs</title>
<script src="https://unpkg.com/htmx.org@1.8.5"></script>
</head>
<body>
<h1>Web Server Logs</h1>
<div id="logs-container">
<!-- Logs will be injected here -->
</div>
<button hx-get="/logs" hx-target="#logs-container" hx-swap="outerHTML">
Load Logs
</button>
<button hx-get="/logs" hx-target="#logs-container" hx-swap="outerHTML" hx-trigger="every 2s">
Auto-Refresh Logs
</button>
</body>
</html>