initial
This commit is contained in:
commit
f21c916eb3
|
@ -0,0 +1,14 @@
|
||||||
|
.PHONY: test
|
||||||
|
|
||||||
|
default: test
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test -v ./...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
goimports -l -w .
|
||||||
|
golangci-lint run --fix
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
sh -c 'test -z "$$(gofmt -l .)"'
|
|
@ -0,0 +1,50 @@
|
||||||
|
# simplelog
|
||||||
|
|
||||||
|
simplelog is an opinionated logging package designed to facilitate easy and
|
||||||
|
structured logging in Go applications with an absolute minimum of
|
||||||
|
boilerplate.
|
||||||
|
|
||||||
|
The idea is that you can add a single import line which replaces the
|
||||||
|
stdlib `log/slog` default handler, and solve the 90% case for logging.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* if output is a tty, outputs pretty color logs
|
||||||
|
* if output is not a tty, outputs json
|
||||||
|
* supports delivering logs via tcp RELP (e.g. to remote rsyslog using imrelp)
|
||||||
|
* supports delivering each log message via a webhook
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To use simplelog, first ensure your project is set up with Go modules:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod init your_project_name
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, add SimpleLog to your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.eeqj.de/sneak/go-simplelog
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Below is an example of how to use SimpleLog in a Go application. This example is provided in the form of a `main.go` file, which demonstrates logging at various levels using structured logging syntax.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
_ "git.eeqj.de/sneak/go-simplelog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// log structured data with slog as usual:
|
||||||
|
slog.Info("User login attempt", slog.String("user", "JohnDoe"), slog.Int("attempt", 3))
|
||||||
|
slog.Warn("Configuration mismatch", slog.String("expected", "config.json"), slog.String("found", "config.dev.json"))
|
||||||
|
slog.Error("Failed to save data", slog.String("reason", "permission denied"))
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,5 @@
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
* fix relp output to cache
|
||||||
|
* add regex for filtering webhook logs
|
||||||
|
* better console output format
|
|
@ -0,0 +1,16 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConsoleHandler struct{}
|
||||||
|
|
||||||
|
func NewConsoleHandler() *ConsoleHandler {
|
||||||
|
return &ConsoleHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConsoleHandler) Log(event Event) error {
|
||||||
|
color.New(color.FgBlue).PrintfFunc()("%s: %s\n", event.Level, event.Message)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
relpServerURL = os.Getenv("LOGGER_RELP_URL")
|
||||||
|
webhookURL = os.Getenv("LOGGER_WEBHOOK_URL")
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomLogger struct {
|
||||||
|
handlers []Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomLogger() *CustomLogger {
|
||||||
|
cl := &CustomLogger{}
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
cl.handlers = append(cl.handlers, NewConsoleHandler())
|
||||||
|
} else {
|
||||||
|
cl.handlers = append(cl.handlers, NewJSONHandler())
|
||||||
|
}
|
||||||
|
if relpServerURL != "" {
|
||||||
|
handler, err := NewRELPHandler(relpServerURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize RELP handler: %v", err)
|
||||||
|
}
|
||||||
|
cl.handlers = append(cl.handlers, handler)
|
||||||
|
}
|
||||||
|
if webhookURL != "" {
|
||||||
|
handler, err := NewWebhookHandler(webhookURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize Webhook handler: %v", err)
|
||||||
|
}
|
||||||
|
cl.handlers = append(cl.handlers, handler)
|
||||||
|
}
|
||||||
|
return cl
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEvent(level, message string, data json.RawMessage) Event {
|
||||||
|
return Event{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Timestamp: time.Now().UTC(),
|
||||||
|
Level: level,
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
module git.eeqj.de/sneak/go-simplelog
|
||||||
|
|
||||||
|
go 1.22.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.17.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/mattn/go-isatty v0.0.20
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||||
|
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
@ -0,0 +1,6 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
// Handler defines the interface for different log outputs.
|
||||||
|
type Handler interface {
|
||||||
|
Log(event Event) error
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONHandler struct{}
|
||||||
|
|
||||||
|
func NewJSONHandler() *JSONHandler {
|
||||||
|
return &JSONHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONHandler) Log(event Event) error {
|
||||||
|
jsonData, _ := json.Marshal(event)
|
||||||
|
log.Println(string(jsonData))
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cacheDir = "/var/cache/relploggercache"
|
||||||
|
diskBufferLimit = 100 // Write to disk after accumulating 100 failed events
|
||||||
|
diskWriteInterval = time.Second // Or write every second, whichever comes first
|
||||||
|
)
|
||||||
|
|
||||||
|
type RELPHandler struct {
|
||||||
|
relpServerURL string
|
||||||
|
conn net.Conn
|
||||||
|
ch chan Event
|
||||||
|
done chan struct{}
|
||||||
|
failedCh chan Event
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRELPHandler(relpURL string) (*RELPHandler, error) {
|
||||||
|
parsedURL, err := url.Parse(relpURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error parsing RELP URL: %v", err)
|
||||||
|
}
|
||||||
|
if parsedURL.Scheme != "tcp" {
|
||||||
|
return nil, fmt.Errorf("RELP URL must have the tcp scheme, got %s", parsedURL.Scheme)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create cache directory: %v", err)
|
||||||
|
}
|
||||||
|
return &RELPHandler{
|
||||||
|
relpServerURL: parsedURL.Host,
|
||||||
|
ch: make(chan Event, diskBufferLimit),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
failedCh: make(chan Event, diskBufferLimit),
|
||||||
|
timer: time.NewTimer(diskWriteInterval),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) Startup() error {
|
||||||
|
var err error
|
||||||
|
r.conn, err = net.Dial("tcp", r.relpServerURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to establish TCP connection: %v", err)
|
||||||
|
}
|
||||||
|
go r.receiveEventsFromChannel()
|
||||||
|
go r.processFailedEvents()
|
||||||
|
go r.watchDirectoryForFailedEventFiles()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) Log(event Event) error {
|
||||||
|
select {
|
||||||
|
case r.ch <- event:
|
||||||
|
return nil // Successfully sent event to channel
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("failed to log event: channel is full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) receiveEventsFromChannel() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-r.ch:
|
||||||
|
if err := r.sendEventToRELPServer(event); err != nil {
|
||||||
|
r.failedCh <- event
|
||||||
|
}
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) processFailedEvents() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-r.failedCh:
|
||||||
|
if err := r.sendEventToRELPServer(event); err != nil {
|
||||||
|
// If still failing, write to disk
|
||||||
|
r.writeFailedToDisk(event)
|
||||||
|
}
|
||||||
|
case <-r.timer.C:
|
||||||
|
// Flush all events in failedCh to disk
|
||||||
|
for len(r.failedCh) > 0 {
|
||||||
|
event := <-r.failedCh
|
||||||
|
r.writeFailedToDisk(event)
|
||||||
|
}
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) sendEventToRELPServer(event Event) error {
|
||||||
|
jsonData, _ := json.Marshal(event)
|
||||||
|
_, err := r.conn.Write(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Implement TCP read for the acknowledgment with a timeout
|
||||||
|
r.conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
ack := make([]byte, 256)
|
||||||
|
n, err := r.conn.Read(ack)
|
||||||
|
if err != nil || string(ack[:n]) != "ACK" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) watchDirectoryForFailedEventFiles() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if r.isConnected() {
|
||||||
|
files, err := ioutil.ReadDir(cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading cache directory: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
filePath := filepath.Join(cacheDir, file.Name())
|
||||||
|
data, err := ioutil.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading file %s: %v", file.Name(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var event Event
|
||||||
|
if err := json.Unmarshal(data, &event); err != nil {
|
||||||
|
log.Printf("Error unmarshalling event from file %s: %v", file.Name(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.ch <- event
|
||||||
|
os.Remove(filePath) // Remove file after processing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Second) // Check disk every 10 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) isConnected() bool {
|
||||||
|
_, err := r.conn.Write([]byte{})
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) Shutdown() error {
|
||||||
|
close(r.done)
|
||||||
|
return r.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RELPHandler) writeFailedToDisk(event Event) {
|
||||||
|
fileName := filepath.Join(cacheDir, uuid.New().String()+".logevent")
|
||||||
|
data, _ := json.Marshal(event)
|
||||||
|
ioutil.WriteFile(fileName, data, 0644)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestCompile checks if the package compiles successfully.
|
||||||
|
func TestCompile(t *testing.T) {
|
||||||
|
// This test ensures that the simplelog package compiles without error.
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
_ "git.eeqj.de/sneak/go-simplelog" // Using underscore to only invoke init()
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Send some test messages with structured data
|
||||||
|
slog.Info("Starting the application", slog.String("status", "initialized"))
|
||||||
|
slog.Info("Attempting to connect to database", slog.String("host", "localhost"), slog.Int("port", 5432))
|
||||||
|
slog.Warn("Using default configuration", slog.String("configuration", "default"))
|
||||||
|
slog.Error("Failed to load module", slog.String("module", "finance"), slog.String("error", "module not found"))
|
||||||
|
slog.Info("Shutting down the application", slog.String("status", "stopped"))
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Set the environment variables for the RELP server URL and optionally for the webhook URL
|
||||||
|
export LOGGER_RELP_URL="tcp://10.201.1.18:20514"
|
||||||
|
export LOGGER_WEBHOOK_URL="https://example.com/webhook"
|
||||||
|
|
||||||
|
# Run the Go program
|
||||||
|
go run relp_log_trial.go
|
|
@ -0,0 +1,33 @@
|
||||||
|
package simplelog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookHandler struct {
|
||||||
|
webhookURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookHandler(webhookURL string) (*WebhookHandler, error) {
|
||||||
|
if _, err := url.ParseRequestURI(webhookURL); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid webhook URL: %v", err)
|
||||||
|
}
|
||||||
|
return &WebhookHandler{webhookURL: webhookURL}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookHandler) Log(event Event) error {
|
||||||
|
jsonData, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling event: %v", err)
|
||||||
|
}
|
||||||
|
response, err := http.Post(w.webhookURL, "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue