1
0
mirror of https://github.com/taigrr/log-socket synced 2026-03-20 16:02:28 -07:00
Files
log-socket/log/log.go
Tai Groot 6a709d3963 feat(log): add colored terminal output without external packages (#16)
Adds ANSI color-coded log levels for terminal output:
- TRACE: Gray
- DEBUG: Cyan
- INFO: Green
- NOTICE: Blue
- WARN: Yellow
- ERROR/PANIC/FATAL: Red (bold for PANIC/FATAL)

Colors are auto-detected based on terminal capability and can be
controlled via SetColorEnabled()/ColorEnabled().

Fixes #9
2026-02-17 03:27:53 -05:00

556 lines
11 KiB
Go

package log
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
)
var (
clients []*Client
sliceTex sync.Mutex
stderrClient *Client
cleanup sync.Once
stderrFinished chan bool
namespaces map[string]bool
namespacesMux sync.RWMutex
)
func init() {
namespaces = make(map[string]bool)
initColorEnabled()
stderrClient = CreateClient(DefaultNamespace)
stderrClient.SetLogLevel(LTrace)
stderrFinished = make(chan bool, 1)
go stderrClient.logStdErr()
}
func (c *Client) logStdErr() {
for e := range c.writer {
if e.level >= c.LogLevel && c.matchesNamespace(e.Namespace) {
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
}
func (c *Client) matchesNamespace(namespace string) bool {
// Empty Namespaces slice means match all
if len(c.Namespaces) == 0 {
return true
}
for _, ns := range c.Namespaces {
if ns == namespace {
return true
}
}
return false
}
func CreateClient(namespaces ...string) *Client {
var client Client
client.initialized = true
client.Namespaces = namespaces
client.writer = make(LogWriter, 1000)
sliceTex.Lock()
clients = append(clients, &client)
sliceTex.Unlock()
return &client
}
func Flush() {
cleanup.Do(func() {
close(stderrClient.writer)
<-stderrFinished
stderrClient.Destroy()
})
}
func (c *Client) Destroy() error {
var otherClients []*Client
if !c.initialized {
panic(errors.New("cannot delete uninitialized client, did you use CreateClient?"))
}
sliceTex.Lock()
c.writer = nil
c.initialized = false
for _, x := range clients {
if x.initialized {
otherClients = append(otherClients, x)
}
}
clients = otherClients
sliceTex.Unlock()
return nil
}
func (c *Client) GetLogLevel() Level {
if !c.initialized {
panic(errors.New("cannot get level for uninitialized client, use CreateClient instead"))
}
return c.LogLevel
}
func createLog(e Entry) {
// Track namespace
namespacesMux.Lock()
namespaces[e.Namespace] = true
namespacesMux.Unlock()
sliceTex.Lock()
for _, c := range clients {
func(c *Client, e Entry) {
// Filter by namespace if client has filters specified
if !c.matchesNamespace(e.Namespace) {
return
}
select {
case c.writer <- e:
// try to clear out one of the older entries
default:
select {
case <-c.writer:
c.writer <- e
default:
}
}
}(c, e)
}
sliceTex.Unlock()
}
// GetNamespaces returns a list of all namespaces that have been used
func GetNamespaces() []string {
namespacesMux.RLock()
defer namespacesMux.RUnlock()
result := make([]string, 0, len(namespaces))
for ns := range namespaces {
result = append(result, ns)
}
return result
}
func SetLogLevel(level Level) {
stderrClient.LogLevel = level
}
// SetLogLevel set log level of logger
func (c *Client) SetLogLevel(level Level) {
if !c.initialized {
panic(errors.New("cannot set level for uninitialized client, use CreateClient instead"))
}
c.LogLevel = level
}
func (c *Client) Get() Entry {
if !c.initialized {
panic(errors.New("cannot get logs for uninitialized client, did you use CreateClient?"))
}
return <-c.writer
}
// Trace prints out logs on trace level
func Trace(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
Namespace: DefaultNamespace,
level: LTrace,
}
createLog(e)
}
// Formatted print for Trace
func Tracef(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
level: LTrace,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Trace prints out logs on trace level with newline
func Traceln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
level: LTrace,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Debug prints out logs on debug level
func Debug(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Debug
func Debugf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Debug prints out logs on debug level with a newline
func Debugln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level
func Info(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Info
func Infof(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level with a newline
func Infoln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level
func Notice(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "NOTICE",
level: LNotice,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Info
func Noticef(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "NOTICE",
level: LNotice,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level with a newline
func Noticeln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "NOTICE",
level: LNotice,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Warn prints out logs on warn level
func Warn(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Warn
func Warnf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Newline print for Warn
func Warnln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Error prints out logs on error level
func Error(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for error
func Errorf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Error prints out logs on error level with a newline
func Errorln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Panic prints out logs on panic level
func Panic(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
Namespace: DefaultNamespace,
}
createLog(e)
if len(args) >= 0 {
switch args[0].(type) {
case error:
panic(args[0])
default:
// falls through to default below
}
}
Flush()
panic(errors.New(output))
}
// Formatted print for panic
func Panicf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
Namespace: DefaultNamespace,
}
createLog(e)
if len(args) >= 0 {
switch args[0].(type) {
case error:
panic(args[0])
default:
// falls through to default below
}
}
Flush()
panic(errors.New(output))
}
func Panicln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
Namespace: DefaultNamespace,
}
createLog(e)
if len(args) >= 0 {
switch args[0].(type) {
case error:
panic(args[0])
default:
// falls through to default below
}
}
Flush()
panic(errors.New(output))
}
// Fatal prints out logs on fatal level
func Fatal(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
Namespace: DefaultNamespace,
}
createLog(e)
Flush()
os.Exit(1)
}
// Formatted print for fatal
func Fatalf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
Namespace: DefaultNamespace,
}
createLog(e)
Flush()
os.Exit(1)
}
func Fatalln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
Namespace: DefaultNamespace,
}
createLog(e)
Flush()
os.Exit(1)
}
func Print(args ...any) {
Info(args...)
}
func Printf(format string, args ...any) {
Infof(format, args...)
}
func Println(args ...any) {
Infoln(args...)
}
// fileInfo for getting which line in which file
func fileInfo(skip int) string {
_, file, line, ok := runtime.Caller(skip)
if !ok {
file = "<???>"
line = 1
} else {
slash := strings.LastIndex(file, "/")
if slash >= 0 {
file = file[slash+1:]
}
}
return fmt.Sprintf("%s:%d", file, line)
}