init, eod commit
This commit is contained in:
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,27 +1,13 @@
|
|||||||
# ---> Go
|
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
|
||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
|
||||||
#
|
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
||||||
|
|
||||||
# Go workspace file
|
|
||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
# env file
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
.idea
|
||||||
|
/.dea/
|
||||||
|
|||||||
14
config.toml
Normal file
14
config.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[log]
|
||||||
|
level = "info"
|
||||||
|
directory = "logs/"
|
||||||
|
rotation = 3 # in days
|
||||||
|
|
||||||
|
[gateway]
|
||||||
|
http_port = ""
|
||||||
|
websocket = ""
|
||||||
|
|
||||||
|
[database]
|
||||||
|
host_dsn = ""
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
database = ""
|
||||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module homestead/homestead_gateway
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
|
github.com/samber/slog-multi v1.6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
)
|
||||||
10
go.sum
Normal file
10
go.sum
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
|
github.com/samber/slog-multi v1.6.0 h1:i1uBY+aaln6ljwdf7Nrt4Sys8Kk6htuYuXDHWJsHtZg=
|
||||||
|
github.com/samber/slog-multi v1.6.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
34
main.go
Normal file
34
main.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"homestead/homestead_gateway/util/config"
|
||||||
|
"homestead/homestead_gateway/util/logger"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfgPath := flag.String("config", "config.toml", "configuration file")
|
||||||
|
|
||||||
|
cfg, err := config.LoadConfig(*cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l, closeFn, err := logger.New("my-service", cfg.Log)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := closeFn(); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "error closing logs: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.SetDefault(l)
|
||||||
|
l.Info("started", "port", 8080)
|
||||||
|
l.Error("something broke", "err", err)
|
||||||
|
}
|
||||||
27
util/config/structs.go
Normal file
27
util/config/structs.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Log LogConfig `toml:"log"`
|
||||||
|
Gateway GatewayConfig `toml:"gateway"`
|
||||||
|
Database DatabaseConfig `toml:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GatewayConfig struct {
|
||||||
|
HttpPort string `toml:"http_port"`
|
||||||
|
Websocket string `toml:"websocket"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level slog.Level `toml:"level"`
|
||||||
|
Directory string `toml:"directory"`
|
||||||
|
Rotation int `toml:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
HostDSN string `toml:"host_dsn"`
|
||||||
|
Username string `toml:"username"`
|
||||||
|
Password string `toml:"password"`
|
||||||
|
Database string `toml:"database"`
|
||||||
|
}
|
||||||
21
util/config/toml.go
Normal file
21
util/config/toml.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err = toml.NewDecoder(file).Decode(&cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
191
util/logger/log.go
Normal file
191
util/logger/log.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"homestead/homestead_gateway/util/config"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
slogmulti "github.com/samber/slog-multi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a logger identified by id.
|
||||||
|
// Returns: *slog.Logger, closeFunc, error.
|
||||||
|
// Call closeFunc() on shutdown to close open files.
|
||||||
|
func New(id string, cfg config.LogConfig) (*slog.Logger, func() error, error) {
|
||||||
|
if cfg.Directory == "" {
|
||||||
|
cfg.Directory = "logs"
|
||||||
|
}
|
||||||
|
if cfg.Rotation <= 0 {
|
||||||
|
cfg.Rotation = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
console := slog.NewTextHandler(&prefixWriter{inner: os.Stderr, prefix: []byte("[" + id + "] "), startLine: true}, &slog.HandlerOptions{AddSource: true})
|
||||||
|
router := newFileRouter(cfg.Directory, cfg.Rotation, id)
|
||||||
|
root := slogmulti.Fanout(console, router)
|
||||||
|
|
||||||
|
return slog.New(root), router.CloseFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prefixWriter) Write(b []byte) (int, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
totalWritten := 0
|
||||||
|
if p.startLine {
|
||||||
|
n, err := p.inner.Write(p.prefix)
|
||||||
|
totalWritten += n
|
||||||
|
if err != nil {
|
||||||
|
return totalWritten, err
|
||||||
|
}
|
||||||
|
p.startLine = false
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := p.inner.Write(b)
|
||||||
|
totalWritten += n
|
||||||
|
if err != nil {
|
||||||
|
return totalWritten, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) > 0 && b[len(b)-1] == '\n' {
|
||||||
|
p.startLine = true
|
||||||
|
}
|
||||||
|
return totalWritten, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileRouter(baseDir string, rotationDays int, id string) *fileRouter {
|
||||||
|
return &fileRouter{
|
||||||
|
handlers: make(map[string]slog.Handler),
|
||||||
|
files: make(map[string]*os.File),
|
||||||
|
baseDir: baseDir,
|
||||||
|
rotationDays: rotationDays,
|
||||||
|
id: id,
|
||||||
|
dirTimeLayout: "2006-01-02", // e.g. 2025-11-30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//goland:noinspection GoUnusedParameter
|
||||||
|
func (r *fileRouter) Enabled(ctx context.Context, lvl slog.Level) bool {
|
||||||
|
// Conservatively true; actual handler will decide.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRouter) Handle(ctx context.Context, rec slog.Record) error {
|
||||||
|
now := time.Now()
|
||||||
|
dirName := now.Format(r.dirTimeLayout)
|
||||||
|
dirPath := filepath.Join(r.baseDir, dirName)
|
||||||
|
filePath := filepath.Join(dirPath, r.id+".log")
|
||||||
|
|
||||||
|
h, err := r.getHandler(dirPath, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file router get handler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Handle(ctx, rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRouter) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fileRouter) WithGroup(name string) slog.Handler {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHandler returns a text handler for the given file, creating dir/file and cleaning up old dirs if needed.
|
||||||
|
func (r *fileRouter) getHandler(dirPath, filePath string) (slog.Handler, error) {
|
||||||
|
r.mu.RLock()
|
||||||
|
h, ok := r.handlers[filePath]
|
||||||
|
r.mu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if h, ok = r.handlers[filePath]; ok {
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, statErr := os.Stat(dirPath)
|
||||||
|
dirExisted := statErr == nil
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dirPath, 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("mkdir %s: %w", dirPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dirExisted {
|
||||||
|
if err := r.cleanupOldDirs(); err != nil {
|
||||||
|
// don't fail logging just because cleanup failed; report to stderr and continue.
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "logger: cleanupOldDirs error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open log file %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
textHandler := slog.NewTextHandler(f, &slog.HandlerOptions{
|
||||||
|
AddSource: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.files[filePath] = f
|
||||||
|
r.handlers[filePath] = textHandler
|
||||||
|
return textHandler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupOldDirs scans r.baseDir for directories matching the dirTimeLayout and deletes any whose
|
||||||
|
// day-start time is older than rotationDays from now.
|
||||||
|
func (r *fileRouter) cleanupOldDirs() error {
|
||||||
|
entries, err := os.ReadDir(r.baseDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read baseDir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
cutoff := now.AddDate(0, 0, -r.rotationDays)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := e.Name()
|
||||||
|
t, err := time.ParseInLocation(r.dirTimeLayout, name, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Before(cutoff) {
|
||||||
|
path := filepath.Join(r.baseDir, name)
|
||||||
|
|
||||||
|
if err := os.RemoveAll(path); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "logger: failed to remove old log dir %s: %v\n", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseFiles closes open files
|
||||||
|
func (r *fileRouter) CloseFiles() error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
var firstErr error
|
||||||
|
for p, f := range r.files {
|
||||||
|
if err := f.Close(); err != nil && firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
delete(r.files, p)
|
||||||
|
delete(r.handlers, p)
|
||||||
|
}
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
27
util/logger/structs.go
Normal file
27
util/logger/structs.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileRouter struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
handlers map[string]slog.Handler // map[filePath]handler
|
||||||
|
files map[string]*os.File // map[filePath]*os.File to close later
|
||||||
|
baseDir string
|
||||||
|
rotationDays int
|
||||||
|
id string
|
||||||
|
dirTimeLayout string // "2006-01-02" - daily dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefixWriter - writes a prefix at the start of each new line.
|
||||||
|
// It is safe for concurrent use.
|
||||||
|
type prefixWriter struct {
|
||||||
|
inner io.Writer
|
||||||
|
prefix []byte
|
||||||
|
mu sync.Mutex
|
||||||
|
startLine bool
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user