mirror of
https://github.com/taigrr/log-socket
synced 2026-03-20 14:52:27 -07:00
Compare commits
1 Commits
cd/6-add-b
...
cd/9-color
| Author | SHA1 | Date | |
|---|---|---|---|
| 0453539d29 |
35
README.md
35
README.md
@@ -15,6 +15,7 @@ A real-time log viewer with WebSocket support and namespace filtering, written i
|
||||
- **Namespace filtering**: Subscribe to specific namespaces via WebSocket
|
||||
- **Frontend namespace selector**: Filter logs by namespace in the web UI
|
||||
- **Namespace API**: GET `/api/namespaces` to list all active namespaces
|
||||
- **Colored terminal output**: Log levels are color-coded in terminal (no external packages)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -188,6 +189,40 @@ ws://localhost:8080/ws?namespaces=api,database # Multiple namespaces
|
||||
- **Color Coding**: Different log levels are color-coded
|
||||
- **Reconnect**: Reconnect WebSocket with new namespace filter
|
||||
|
||||
## Terminal Colors
|
||||
|
||||
Log output to stderr is automatically colorized when writing to a terminal. Colors are disabled when output is piped or redirected to a file.
|
||||
|
||||
### Color Scheme
|
||||
|
||||
| Level | Color |
|
||||
|--------|--------------|
|
||||
| TRACE | Gray |
|
||||
| DEBUG | Cyan |
|
||||
| INFO | Green |
|
||||
| NOTICE | Blue |
|
||||
| WARN | Yellow |
|
||||
| ERROR | Red |
|
||||
| PANIC | Bold Red |
|
||||
| FATAL | Bold Red |
|
||||
|
||||
### Controlling Colors
|
||||
|
||||
```go
|
||||
// Disable colors (e.g., for CI/CD or file output)
|
||||
logger.SetColorEnabled(false)
|
||||
|
||||
// Enable colors explicitly
|
||||
logger.SetColorEnabled(true)
|
||||
|
||||
// Check current state
|
||||
if logger.ColorEnabled() {
|
||||
// colors are on
|
||||
}
|
||||
```
|
||||
|
||||
Colors are implemented using standard ANSI escape codes with no external dependencies.
|
||||
|
||||
## Migration from v1
|
||||
|
||||
### Import Path
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// drainClient continuously reads from a client to prevent blocking.
|
||||
// Returns a function to stop draining and wait for completion.
|
||||
func drainClient(c *Client) func() {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-c.writer:
|
||||
// Drain entries
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() { close(done) }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Serial Benchmarks - Single Log Levels
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkTrace(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LTrace)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Trace("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDebug(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LDebug)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Debug("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfo(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Info("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNotice(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LNotice)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Notice("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWarn(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LWarn)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Warn("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkError(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LError)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Error("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Formatted Logging Benchmarks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkDebugf(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LDebug)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Debugf("benchmark message %d with %s", i, "formatting")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfof(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Infof("benchmark message %d with %s", i, "formatting")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErrorf(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LError)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Errorf("benchmark message %d with %s", i, "formatting")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Parallel Benchmarks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkDebugParallel(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LDebug)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
Debug("parallel benchmark message")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkInfoParallel(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
Info("parallel benchmark message")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkInfofParallel(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
counter := 0
|
||||
for pb.Next() {
|
||||
Infof("parallel benchmark message %d", counter)
|
||||
counter++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Logger Instance Benchmarks (Namespaced Logging)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkLoggerInfo(b *testing.B) {
|
||||
logger := NewLogger("benchmark")
|
||||
c := CreateClient("benchmark")
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoggerInfof(b *testing.B) {
|
||||
logger := NewLogger("benchmark")
|
||||
c := CreateClient("benchmark")
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Infof("benchmark message %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoggerDebugParallel(b *testing.B) {
|
||||
logger := NewLogger("benchmark")
|
||||
c := CreateClient("benchmark")
|
||||
c.SetLogLevel(LDebug)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
logger.Debug("parallel benchmark message")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Multiple Client Benchmarks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkMultipleClients(b *testing.B) {
|
||||
const numClients = 5
|
||||
var clients []*Client
|
||||
var stopFuncs []func()
|
||||
|
||||
for i := 0; i < numClients; i++ {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
clients = append(clients, c)
|
||||
stopFuncs = append(stopFuncs, stop)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for i, c := range clients {
|
||||
stopFuncs[i]()
|
||||
c.Destroy()
|
||||
}
|
||||
}()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Info("benchmark message to multiple clients")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMultipleNamespaces(b *testing.B) {
|
||||
namespaces := []string{"auth", "db", "api", "cache", "queue"}
|
||||
var loggers []*Logger
|
||||
var clients []*Client
|
||||
var stopFuncs []func()
|
||||
|
||||
for _, ns := range namespaces {
|
||||
loggers = append(loggers, NewLogger(ns))
|
||||
c := CreateClient(ns)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
clients = append(clients, c)
|
||||
stopFuncs = append(stopFuncs, stop)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for i, c := range clients {
|
||||
stopFuncs[i]()
|
||||
c.Destroy()
|
||||
}
|
||||
}()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
loggers[i%len(loggers)].Info("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// With Synchronous Client Consumption (Legacy Pattern)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// BenchmarkDebugWithSyncConsumer measures the overhead of synchronous consumption
|
||||
// using the Get() method, processing one message at a time.
|
||||
func BenchmarkDebugWithSyncConsumer(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LDebug)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
// This benchmark measures just the logging side with async draining.
|
||||
// For sync consumption patterns, see the TestOrder test.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Debug("benchmark message")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Comparison Benchmarks (Different Message Sizes)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkInfoShortMessage(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Info("ok")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfoMediumMessage(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
msg := "This is a medium-length log message for benchmarking purposes"
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfoLongMessage(b *testing.B) {
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LInfo)
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
msg := "This is a much longer log message that simulates real-world logging scenarios where developers tend to include more context about what the application is doing, including variable values, request IDs, and other debugging information that can be quite verbose"
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Overhead Benchmarks (Level Filtering)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkDebugFilteredByLevel(b *testing.B) {
|
||||
// No client with Debug level, so Debug logs won't be consumed
|
||||
// This measures the overhead of creating log entries that get filtered
|
||||
c := CreateClient(DefaultNamespace)
|
||||
c.SetLogLevel(LError) // Only Error and above
|
||||
stop := drainClient(c)
|
||||
defer stop()
|
||||
defer c.Destroy()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Debug("this message will be filtered")
|
||||
}
|
||||
}
|
||||
99
log/color.go
Normal file
99
log/color.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ANSI color codes for terminal output
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorRed = "\033[31m"
|
||||
colorGreen = "\033[32m"
|
||||
colorYellow = "\033[33m"
|
||||
colorBlue = "\033[34m"
|
||||
colorPurple = "\033[35m"
|
||||
colorCyan = "\033[36m"
|
||||
colorWhite = "\033[37m"
|
||||
colorGray = "\033[90m"
|
||||
|
||||
// Bold variants
|
||||
colorBoldRed = "\033[1;31m"
|
||||
colorBoldYellow = "\033[1;33m"
|
||||
colorBoldWhite = "\033[1;37m"
|
||||
)
|
||||
|
||||
var (
|
||||
colorEnabled = true
|
||||
colorEnabledOnce sync.Once
|
||||
colorMux sync.RWMutex
|
||||
)
|
||||
|
||||
// SetColorEnabled enables or disables colored output for stderr logging.
|
||||
// By default, color is enabled when stderr is a terminal.
|
||||
func SetColorEnabled(enabled bool) {
|
||||
colorMux.Lock()
|
||||
colorEnabled = enabled
|
||||
colorMux.Unlock()
|
||||
}
|
||||
|
||||
// ColorEnabled returns whether colored output is currently enabled.
|
||||
func ColorEnabled() bool {
|
||||
colorMux.RLock()
|
||||
defer colorMux.RUnlock()
|
||||
return colorEnabled
|
||||
}
|
||||
|
||||
// isTerminal checks if the given file descriptor is a terminal.
|
||||
// This is a simple heuristic that works on Unix-like systems.
|
||||
func isTerminal(f *os.File) bool {
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (stat.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
// initColorEnabled sets the default color state based on whether stderr is a terminal.
|
||||
func initColorEnabled() {
|
||||
colorEnabledOnce.Do(func() {
|
||||
colorEnabled = isTerminal(os.Stderr)
|
||||
})
|
||||
}
|
||||
|
||||
// levelColor returns the ANSI color code for a given log level.
|
||||
func levelColor(level Level) string {
|
||||
switch level {
|
||||
case LTrace:
|
||||
return colorGray
|
||||
case LDebug:
|
||||
return colorCyan
|
||||
case LInfo:
|
||||
return colorGreen
|
||||
case LNotice:
|
||||
return colorBlue
|
||||
case LWarn:
|
||||
return colorYellow
|
||||
case LError:
|
||||
return colorRed
|
||||
case LPanic:
|
||||
return colorBoldRed
|
||||
case LFatal:
|
||||
return colorBoldRed
|
||||
default:
|
||||
return colorReset
|
||||
}
|
||||
}
|
||||
|
||||
// colorize wraps text with ANSI color codes if color is enabled.
|
||||
func colorize(text string, color string) string {
|
||||
if !ColorEnabled() {
|
||||
return text
|
||||
}
|
||||
return color + text + colorReset
|
||||
}
|
||||
|
||||
// colorizeLevelText returns the level string with appropriate color.
|
||||
func colorizeLevelText(level string, lvl Level) string {
|
||||
return colorize(level, levelColor(lvl))
|
||||
}
|
||||
10
log/log.go
10
log/log.go
@@ -22,6 +22,7 @@ var (
|
||||
|
||||
func init() {
|
||||
namespaces = make(map[string]bool)
|
||||
initColorEnabled()
|
||||
stderrClient = CreateClient(DefaultNamespace)
|
||||
stderrClient.SetLogLevel(LTrace)
|
||||
stderrFinished = make(chan bool, 1)
|
||||
@@ -31,7 +32,10 @@ func init() {
|
||||
func (c *Client) logStdErr() {
|
||||
for e := range c.writer {
|
||||
if e.level >= c.LogLevel && c.matchesNamespace(e.Namespace) {
|
||||
fmt.Fprintf(os.Stderr, "%s\t%s\t[%s]\t%s\t%s\n", e.Timestamp.String(), e.Level, e.Namespace, e.Output, e.File)
|
||||
levelStr := colorizeLevelText(e.Level, e.level)
|
||||
nsStr := colorize("["+e.Namespace+"]", colorPurple)
|
||||
fileStr := colorize(e.File, colorGray)
|
||||
fmt.Fprintf(os.Stderr, "%s\t%s\t%s\t%s\t%s\n", e.Timestamp.String(), levelStr, nsStr, e.Output, fileStr)
|
||||
}
|
||||
}
|
||||
stderrFinished <- true
|
||||
@@ -99,7 +103,7 @@ func createLog(e Entry) {
|
||||
namespacesMux.Lock()
|
||||
namespaces[e.Namespace] = true
|
||||
namespacesMux.Unlock()
|
||||
|
||||
|
||||
sliceTex.Lock()
|
||||
for _, c := range clients {
|
||||
func(c *Client, e Entry) {
|
||||
@@ -126,7 +130,7 @@ func createLog(e Entry) {
|
||||
func GetNamespaces() []string {
|
||||
namespacesMux.RLock()
|
||||
defer namespacesMux.RUnlock()
|
||||
|
||||
|
||||
result := make([]string, 0, len(namespaces))
|
||||
for ns := range namespaces {
|
||||
result = append(result, ns)
|
||||
|
||||
@@ -2,6 +2,7 @@ package log
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -36,6 +37,21 @@ func TestSetLogLevel(t *testing.T) {
|
||||
c.Destroy()
|
||||
}
|
||||
|
||||
func BenchmarkDebugSerial(b *testing.B) {
|
||||
c := CreateClient("test")
|
||||
var x sync.WaitGroup
|
||||
x.Add(b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
Debug(i)
|
||||
go func() {
|
||||
c.Get()
|
||||
x.Done()
|
||||
}()
|
||||
}
|
||||
x.Wait()
|
||||
c.Destroy()
|
||||
}
|
||||
|
||||
// Trace ensure logs come out in the right order
|
||||
func TestOrder(t *testing.T) {
|
||||
testString := "Testing trace: "
|
||||
|
||||
10
main.go
10
main.go
@@ -17,21 +17,21 @@ func generateLogs() {
|
||||
apiLogger := logger.NewLogger("api")
|
||||
dbLogger := logger.NewLogger("database")
|
||||
authLogger := logger.NewLogger("auth")
|
||||
|
||||
|
||||
for {
|
||||
logger.Info("This is a default namespace log!")
|
||||
apiLogger.Info("API request received")
|
||||
apiLogger.Debug("Processing API call")
|
||||
|
||||
|
||||
dbLogger.Info("Database query executed")
|
||||
dbLogger.Warn("Slow query detected")
|
||||
|
||||
|
||||
authLogger.Info("User authentication successful")
|
||||
authLogger.Error("Failed login attempt detected")
|
||||
|
||||
|
||||
logger.Trace("This is a trace log in default namespace!")
|
||||
logger.Warn("This is a warning in default namespace!")
|
||||
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func LogSocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if namespacesParam != "" {
|
||||
namespaces = strings.Split(namespacesParam, ",")
|
||||
}
|
||||
|
||||
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
logger.Error("upgrade:", err)
|
||||
|
||||
Reference in New Issue
Block a user