feat(logging): added in db and logging with websocket
This commit is contained in:
@@ -1,65 +1,149 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Configure clients
|
builder.Services.AddHttpClient("GoBackend", client =>
|
||||||
builder.Services.AddHttpClient("GoBackend", client => {
|
{
|
||||||
client.BaseAddress = new Uri("http://localhost:8080");
|
client.BaseAddress = new Uri("http://localhost:8080");
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Handle trailing slash redirects
|
// Enable WebSocket support
|
||||||
app.Use(async (context, next) => {
|
app.UseWebSockets();
|
||||||
if (context.Request.Path.Equals("/lst", StringComparison.OrdinalIgnoreCase)) {
|
|
||||||
context.Response.Redirect("/lst/", permanent: true);
|
app.Use(async (context, next) =>
|
||||||
return;
|
{
|
||||||
|
// Proxy WebSocket requests for /lst/api/logger/logs (adjust path as needed)
|
||||||
|
if (context.WebSockets.IsWebSocketRequest &&
|
||||||
|
context.Request.Path.StartsWithSegments("/lst/api/logger/logs"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var backendUri = new UriBuilder("ws", "localhost", 8080)
|
||||||
|
{
|
||||||
|
Path = context.Request.Path,
|
||||||
|
Query = context.Request.QueryString.ToString()
|
||||||
|
}.Uri;
|
||||||
|
|
||||||
|
using var backendSocket = new ClientWebSocket();
|
||||||
|
|
||||||
|
// Forward most headers except those managed by WebSocket protocol
|
||||||
|
foreach (var header in context.Request.Headers)
|
||||||
|
{
|
||||||
|
if (!header.Key.Equals("Host", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!header.Key.Equals("Upgrade", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!header.Key.Equals("Connection", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!header.Key.Equals("Sec-WebSocket-Key", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!header.Key.Equals("Sec-WebSocket-Version", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
backendSocket.Options.SetRequestHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await backendSocket.ConnectAsync(backendUri, context.RequestAborted);
|
||||||
|
|
||||||
|
using var frontendSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// Bidirectional forwarding tasks
|
||||||
|
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
|
||||||
|
var forwardToFrontend = ForwardWebSocketAsync(backendSocket, frontendSocket, cts.Token);
|
||||||
|
|
||||||
|
await Task.WhenAny(forwardToBackend, forwardToFrontend);
|
||||||
|
cts.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
|
||||||
|
await context.Response.WriteAsync($"WebSocket proxy error: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Proxy all requests to Go backend
|
// Proxy normal HTTP requests
|
||||||
app.Use(async (context, next) => {
|
app.Use(async (context, next) =>
|
||||||
// Skip special paths
|
{
|
||||||
if (context.Request.Path.StartsWithSegments("/.well-known")) {
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>()
|
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient("GoBackend");
|
||||||
.CreateClient("GoBackend");
|
|
||||||
|
try
|
||||||
try {
|
{
|
||||||
var request = new HttpRequestMessage(
|
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method),
|
||||||
new HttpMethod(context.Request.Method),
|
|
||||||
context.Request.Path + context.Request.QueryString);
|
context.Request.Path + context.Request.QueryString);
|
||||||
|
|
||||||
// Copy headers
|
foreach (var header in context.Request.Headers)
|
||||||
foreach (var header in context.Request.Headers) {
|
{
|
||||||
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) {
|
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
|
||||||
|
{
|
||||||
request.Content ??= new StreamContent(context.Request.Body);
|
request.Content ??= new StreamContent(context.Request.Body);
|
||||||
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
|
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.Request.ContentLength > 0) {
|
if (context.Request.ContentLength > 0 && request.Content == null)
|
||||||
|
{
|
||||||
request.Content = new StreamContent(context.Request.Body);
|
request.Content = new StreamContent(context.Request.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
||||||
|
|
||||||
context.Response.StatusCode = (int)response.StatusCode;
|
context.Response.StatusCode = (int)response.StatusCode;
|
||||||
|
|
||||||
foreach (var header in response.Headers) {
|
foreach (var header in response.Headers)
|
||||||
|
{
|
||||||
|
context.Response.Headers[header.Key] = header.Value.ToArray();
|
||||||
|
}
|
||||||
|
foreach (var header in response.Content.Headers)
|
||||||
|
{
|
||||||
context.Response.Headers[header.Key] = header.Value.ToArray();
|
context.Response.Headers[header.Key] = header.Value.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.Content.Headers.ContentType != null) {
|
context.Response.Headers.Remove("transfer-encoding");
|
||||||
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
await response.Content.CopyToAsync(context.Response.Body);
|
await response.Content.CopyToAsync(context.Response.Body);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException) {
|
catch (HttpRequestException ex)
|
||||||
context.Response.StatusCode = 502;
|
{
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
|
||||||
|
await context.Response.WriteAsync($"Backend request failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var buffer = new byte[4 * 1024];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (source.State == WebSocketState.Open &&
|
||||||
|
destination.State == WebSocketState.Open &&
|
||||||
|
!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await destination.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WebSocketException)
|
||||||
|
{
|
||||||
|
// Normal close or network error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|||||||
@@ -1,36 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<system.webServer>
|
<system.webServer>
|
||||||
|
<!-- Enable WebSockets -->
|
||||||
|
<webSocket enabled="true" receiveBufferLimit="4194304" pingInterval="00:01:00" />
|
||||||
|
|
||||||
<rewrite>
|
<rewrite>
|
||||||
<rules>
|
<rules>
|
||||||
<!-- Redirect root to /lst/ -->
|
<!-- Proxy all requests starting with /lst/ to the .NET wrapper (port 4000) -->
|
||||||
<rule name="Root Redirect" stopProcessing="true">
|
<rule name="Proxy to Wrapper" stopProcessing="true">
|
||||||
<match url="^$" />
|
|
||||||
<action type="Redirect" url="/lst/" redirectType="Permanent" />
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- Proxy static assets -->
|
|
||||||
<rule name="Static Assets" stopProcessing="true">
|
|
||||||
<match url="^lst/assets/(.*)" />
|
|
||||||
<action type="Rewrite" url="http://localhost:8080/lst/assets/{R:1}" />
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- Proxy API requests -->
|
|
||||||
<rule name="API Routes" stopProcessing="true">
|
|
||||||
<match url="^lst/api/(.*)" />
|
|
||||||
<action type="Rewrite" url="http://localhost:8080/lst/api/{R:1}" />
|
|
||||||
</rule>
|
|
||||||
|
|
||||||
<!-- Proxy all other requests -->
|
|
||||||
<rule name="Frontend Routes" stopProcessing="true">
|
|
||||||
<match url="^lst/(.*)" />
|
<match url="^lst/(.*)" />
|
||||||
<action type="Rewrite" url="http://localhost:8080/lst/{R:1}" />
|
<action type="Rewrite" url="http://localhost:8080/{R:1}" />
|
||||||
</rule>
|
</rule>
|
||||||
</rules>
|
</rules>
|
||||||
</rewrite>
|
</rewrite>
|
||||||
|
|
||||||
<staticContent>
|
<staticContent>
|
||||||
<clear />
|
|
||||||
<mimeMap fileExtension=".js" mimeType="application/javascript" />
|
<mimeMap fileExtension=".js" mimeType="application/javascript" />
|
||||||
<mimeMap fileExtension=".mjs" mimeType="application/javascript" />
|
<mimeMap fileExtension=".mjs" mimeType="application/javascript" />
|
||||||
<mimeMap fileExtension=".css" mimeType="text/css" />
|
<mimeMap fileExtension=".css" mimeType="text/css" />
|
||||||
@@ -38,9 +22,11 @@
|
|||||||
</staticContent>
|
</staticContent>
|
||||||
|
|
||||||
<handlers>
|
<handlers>
|
||||||
|
<!-- Let AspNetCoreModule handle all requests -->
|
||||||
|
<remove name="WebSocketHandler" />
|
||||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
|
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
|
||||||
</handlers>
|
</handlers>
|
||||||
|
|
||||||
<aspNetCore processPath="dotnet" arguments=".\LstWrapper.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
|
<aspNetCore processPath="dotnet" arguments=".\LstWrapper.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
|
||||||
</system.webServer>
|
</system.webServer>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
111
backend/cmd/services/logging/createLog.go
Normal file
111
backend/cmd/services/logging/createLog.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"lst.net/utils/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomLogger struct {
|
||||||
|
consoleLogger zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a configured CustomLogger.
|
||||||
|
func New() *CustomLogger {
|
||||||
|
// Colorized console output
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CustomLogger{
|
||||||
|
consoleLogger: zerolog.New(consoleWriter).
|
||||||
|
With().
|
||||||
|
Timestamp().
|
||||||
|
Logger(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrettyFormat(level, message string, metadata map[string]interface{}) string {
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
base := fmt.Sprintf("[%s] %s| Message: %s", strings.ToUpper(level), timestamp, message)
|
||||||
|
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
metaJSON, _ := json.Marshal(metadata)
|
||||||
|
return fmt.Sprintf("%s | Metadata: %s", base, string(metaJSON))
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) logToPostgres(level, message, service string, metadata map[string]interface{}) {
|
||||||
|
err := db.CreateLog(level, message, service, metadata)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to console if DB fails
|
||||||
|
log.Error().Err(err).Msg("Failed to write log to PostgreSQL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Level-Specific Methods ---
|
||||||
|
|
||||||
|
func (l *CustomLogger) Info(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Info().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("info", message, service, fields)
|
||||||
|
|
||||||
|
PostLog(PrettyFormat("info", message, fields)) // Broadcast pretty message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Warn(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Error().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("warn", message, service, fields)
|
||||||
|
|
||||||
|
PostLog(PrettyFormat("warn", message, fields)) // Broadcast pretty message
|
||||||
|
|
||||||
|
// Custom logic for errors (e.g., alerting)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
l.consoleLogger.Warn().Msg("Additional error context captured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Error(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Error().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("error", message, service, fields)
|
||||||
|
|
||||||
|
PostLog(PrettyFormat("error", message, fields)) // Broadcast pretty message
|
||||||
|
|
||||||
|
// Custom logic for errors (e.g., alerting)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
l.consoleLogger.Warn().Msg("Additional error context captured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Panic(message, service string, fields map[string]interface{}) {
|
||||||
|
// Log to console (colored, with fields)
|
||||||
|
l.consoleLogger.Error().
|
||||||
|
Str("service", service).
|
||||||
|
Fields(fields).
|
||||||
|
Msg(message + " (PANIC)") // Explicitly mark as panic
|
||||||
|
|
||||||
|
// Log to PostgreSQL (sync to ensure it's saved before crashing)
|
||||||
|
err := db.CreateLog("panic", message, service, fields) // isCritical=true
|
||||||
|
if err != nil {
|
||||||
|
l.consoleLogger.Error().Err(err).Msg("Failed to save panic log to PostgreSQL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional context (optional)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
l.consoleLogger.Warn().Msg("Additional panic context captured")
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Debug(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Debug().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("debug", message, service, fields)
|
||||||
|
}
|
||||||
12
backend/cmd/services/logging/logger.go
Normal file
12
backend/cmd/services/logging/logger.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterLoggerRoutes(l *gin.Engine, baseUrl string) {
|
||||||
|
|
||||||
|
configGroup := l.Group(baseUrl + "/api/logger")
|
||||||
|
configGroup.GET("/logs", GetLogs)
|
||||||
|
|
||||||
|
}
|
||||||
132
backend/cmd/services/logging/logs_get_route.go
Normal file
132
backend/cmd/services/logging/logs_get_route.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logChannel = make(chan string, 1000) // Buffered channel for new logs
|
||||||
|
wsClients = make(map[*websocket.Conn]bool)
|
||||||
|
wsClientsMux sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
fmt.Println("Origin:", r.Header.Get("Origin"))
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostLog sends a new log to all connected SSE clients
|
||||||
|
func PostLog(message string) {
|
||||||
|
// Send to SSE channel
|
||||||
|
select {
|
||||||
|
case logChannel <- message:
|
||||||
|
log.Printf("Published to SSE: %s", message)
|
||||||
|
default:
|
||||||
|
log.Printf("DROPPED SSE message (channel full): %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
wsClientsMux.Lock()
|
||||||
|
defer wsClientsMux.Unlock()
|
||||||
|
for client := range wsClients {
|
||||||
|
err := client.WriteMessage(websocket.TextMessage, []byte(message))
|
||||||
|
if err != nil {
|
||||||
|
client.Close()
|
||||||
|
delete(wsClients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogs(c *gin.Context) {
|
||||||
|
// Check if it's a WebSocket request
|
||||||
|
if websocket.IsWebSocketUpgrade(c.Request) {
|
||||||
|
handleWebSocket(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, handle as SSE
|
||||||
|
handleSSE(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(c *gin.Context) {
|
||||||
|
log := New()
|
||||||
|
log.Info("SSE connection established", "logger", map[string]interface{}{
|
||||||
|
"endpoint": "/api/logger/logs",
|
||||||
|
"client_ip": c.ClientIP(),
|
||||||
|
"user_agent": c.Request.UserAgent(),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/event-stream")
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Header("Connection", "keep-alive")
|
||||||
|
|
||||||
|
flusher, ok := c.Writer.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
log.Info("SSE not supported", "logger", nil)
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := c.Writer.CloseNotify()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-notify:
|
||||||
|
log.Info("SSE client disconnected", "logger", nil)
|
||||||
|
return
|
||||||
|
case message := <-logChannel:
|
||||||
|
fmt.Fprintf(c.Writer, "data: %s\n\n", message)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebSocket(c *gin.Context) {
|
||||||
|
log := New()
|
||||||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("WebSocket upgrade failed", "logger", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register client
|
||||||
|
wsClientsMux.Lock()
|
||||||
|
wsClients[conn] = true
|
||||||
|
wsClientsMux.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
wsClientsMux.Lock()
|
||||||
|
delete(wsClients, conn)
|
||||||
|
wsClientsMux.Unlock()
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
log.Info("WebSocket client disconnected", "logger", map[string]interface{}{})
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Keep connection alive (or optionally echo, or wait for pings)
|
||||||
|
for {
|
||||||
|
// Can just read to keep the connection alive
|
||||||
|
if _, _, err := conn.NextReader(); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func sendRecentLogs(conn *websocket.Conn) {
|
||||||
|
// // Implement your logic to get recent logs from DB or buffer
|
||||||
|
// recentLogs := getLast20Logs()
|
||||||
|
// for _, log := range recentLogs {
|
||||||
|
// conn.WriteMessage(websocket.TextMessage, []byte(log))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -3,35 +3,57 @@ module lst.net
|
|||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gin-contrib/cors v1.7.6
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/swaggo/swag v1.16.5
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic v1.13.3 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.19.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.27.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,63 @@
|
|||||||
|
// @title My Awesome API
|
||||||
|
// @version 1.0
|
||||||
|
// @description This is a sample server for a pet store.
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.url http://www.swagger.io/support
|
||||||
|
// @contact.email support@swagger.io
|
||||||
|
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /api/v1
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"lst.net/cmd/services/logging"
|
||||||
|
_ "lst.net/docs"
|
||||||
|
"lst.net/utils/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
log := logging.New()
|
||||||
// Load .env only in dev (not Docker/production)
|
// Load .env only in dev (not Docker/production)
|
||||||
if os.Getenv("RUNNING_IN_DOCKER") != "true" {
|
if os.Getenv("RUNNING_IN_DOCKER") != "true" {
|
||||||
err := godotenv.Load("../.env")
|
err := godotenv.Load("../.env")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Warning: .env file not found (ok in Docker/production)")
|
log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize DB
|
||||||
|
if err := db.InitDB(); err != nil {
|
||||||
|
log.Panic("Database intialize failed", "db", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"casue": errors.Unwrap(err),
|
||||||
|
"timeout": "30s",
|
||||||
|
"details": fmt.Sprintf("%+v", err), // Full stack trace if available
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
sqlDB, _ := db.DB.DB()
|
||||||
|
sqlDB.Close()
|
||||||
|
log.Error("Recovered from panic during DB shutdown", "db", map[string]interface{}{
|
||||||
|
"panic": r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Set basePath dynamically
|
// Set basePath dynamically
|
||||||
basePath := "/"
|
basePath := "/"
|
||||||
|
|
||||||
@@ -34,6 +73,16 @@ func main() {
|
|||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable CORS (adjust origins as needed)
|
||||||
|
r.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: []string{"*"}, // Allow all origins (change in production)
|
||||||
|
AllowMethods: []string{"GET", "OPTIONS", "POST", "DELETE", "PATCH", "CONNECT"},
|
||||||
|
AllowHeaders: []string{"Origin", "Cache-Control", "Content-Type"},
|
||||||
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
AllowWebSockets: true,
|
||||||
|
}))
|
||||||
|
|
||||||
// // --- Add Redirects Here ---
|
// // --- Add Redirects Here ---
|
||||||
// // Redirect root ("/") to "/app" or "/lst/app"
|
// // Redirect root ("/") to "/app" or "/lst/app"
|
||||||
// r.GET("/", func(c *gin.Context) {
|
// r.GET("/", func(c *gin.Context) {
|
||||||
@@ -52,34 +101,18 @@ func main() {
|
|||||||
r.StaticFS(basePath+"/app", http.Dir("frontend"))
|
r.StaticFS(basePath+"/app", http.Dir("frontend"))
|
||||||
|
|
||||||
r.GET(basePath+"/api/ping", func(c *gin.Context) {
|
r.GET(basePath+"/api/ping", func(c *gin.Context) {
|
||||||
|
log.Info("Checking if the server is up", "system", map[string]interface{}{
|
||||||
|
"endpoint": "/api/ping",
|
||||||
|
"client_ip": c.ClientIP(),
|
||||||
|
"user_agent": c.Request.UserAgent(),
|
||||||
|
})
|
||||||
c.JSON(200, gin.H{"message": "pong"})
|
c.JSON(200, gin.H{"message": "pong"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logging.RegisterLoggerRoutes(r, basePath)
|
||||||
|
|
||||||
r.Any(basePath+"/api", errorApiLoc)
|
r.Any(basePath+"/api", errorApiLoc)
|
||||||
|
|
||||||
// // Serve static assets for Vite app
|
|
||||||
// r.Static("/lst/app/assets", "./dist/app/assets")
|
|
||||||
|
|
||||||
// // Catch-all for Vite app routes
|
|
||||||
// r.NoRoute(func(c *gin.Context) {
|
|
||||||
// path := c.Request.URL.Path
|
|
||||||
|
|
||||||
// // Don't handle API, assets, or docs
|
|
||||||
// if strings.HasPrefix(path, "/lst/api") ||
|
|
||||||
// strings.HasPrefix(path, "/lst/app/assets") ||
|
|
||||||
// strings.HasPrefix(path, "/lst/docs") {
|
|
||||||
// c.JSON(404, gin.H{"error": "Not found"})
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Serve index.html for all /lst/app routes
|
|
||||||
// if strings.HasPrefix(path, "/lst/app") {
|
|
||||||
// c.File("./dist/app/index.html")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// c.JSON(404, gin.H{"error": "Not found"})
|
|
||||||
// })
|
|
||||||
r.Run(":8080")
|
r.Run(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,5 +126,11 @@ func main() {
|
|||||||
// c.JSON(http.StatusBadRequest, gin.H{"message": "welcome to lst system you might have just encountered an incorrect area of the app"})
|
// c.JSON(http.StatusBadRequest, gin.H{"message": "welcome to lst system you might have just encountered an incorrect area of the app"})
|
||||||
// }
|
// }
|
||||||
func errorApiLoc(c *gin.Context) {
|
func errorApiLoc(c *gin.Context) {
|
||||||
|
log := logging.New()
|
||||||
|
log.Info("Api endpoint hit that dose not exist", "system", map[string]interface{}{
|
||||||
|
"endpoint": "/api",
|
||||||
|
"client_ip": c.ClientIP(),
|
||||||
|
"user_agent": c.Request.UserAgent(),
|
||||||
|
})
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered an api route that dose not exist"})
|
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered an api route that dose not exist"})
|
||||||
}
|
}
|
||||||
|
|||||||
32
backend/utils/db/db.go
Normal file
32
backend/utils/db/db.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
func InitDB() error {
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s",
|
||||||
|
os.Getenv("DB_HOST"),
|
||||||
|
os.Getenv("DB_PORT"),
|
||||||
|
os.Getenv("DB_USER"),
|
||||||
|
os.Getenv("DB_PASSWORD"),
|
||||||
|
os.Getenv("DB_NAME"))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-migrate all models
|
||||||
|
DB.AutoMigrate(&Log{}) // Add other models here
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
41
backend/utils/db/logs.go
Normal file
41
backend/utils/db/logs.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Level string `gorm:"size:10;not null"` // "info", "error", etc.
|
||||||
|
Message string `gorm:"not null"`
|
||||||
|
Service string `gorm:"size:50"` // Optional: service name
|
||||||
|
Metadata JSONB `gorm:"type:jsonb"` // Structured fields (e.g., {"user_id": 123})
|
||||||
|
CreatedAt time.Time `gorm:"index"` // Auto-set by GORM
|
||||||
|
Checked bool `gorm:"type:boolean;default:false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONB is a helper type for PostgreSQL JSONB fields.
|
||||||
|
type JSONB map[string]interface{}
|
||||||
|
|
||||||
|
// --- CRUD Operations ---
|
||||||
|
|
||||||
|
// CreateLog inserts a new log entry.
|
||||||
|
func CreateLog(level, message, service string, metadata JSONB) error {
|
||||||
|
log := Log{
|
||||||
|
Level: level,
|
||||||
|
Message: message,
|
||||||
|
Service: service,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
return DB.Create(&log).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogsByLevel fetches logs filtered by severity.
|
||||||
|
func GetLogsByLevel(level string, limit int) ([]Log, error) {
|
||||||
|
var logs []Log
|
||||||
|
err := DB.Where("level = ?", level).Limit(limit).Find(&logs).Error
|
||||||
|
return logs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOldLogs removes logs older than `days`.
|
||||||
|
func DeleteOldLogs(days int) error {
|
||||||
|
return DB.Where("created_at < ?", time.Now().AddDate(0, 0, -days)).Delete(&Log{}).Error
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user