Skip to content

Commit 38a9901

Browse files
committed
chore: massive optimization overhaul
1 parent 88772da commit 38a9901

File tree

6 files changed

+231
-27
lines changed

6 files changed

+231
-27
lines changed

internal/config/config.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@ type SSLConfig struct {
2525

2626
// SiteConfig contains the configuration for a single site.
2727
type SiteConfig struct {
28-
Domain string `json:"domain"`
29-
Port int `json:"port"`
30-
RootDirectory string `json:"root_directory"`
31-
CustomHeaders map[string]string `json:"custom_headers"`
32-
ProxyPass string `json:"proxy_pass"`
33-
SSL SSLConfig `json:"ssl"`
34-
RequestTimeout int `json:"request_timeout"` // in seconds
35-
FlushInterval string `json:"proxy_flush_interval"`
36-
BufferSizeKB int `json:"buffer_size_kb"`
28+
Domain string `json:"domain"`
29+
Port int `json:"port"`
30+
RootDirectory string `json:"root_directory"`
31+
CustomHeaders map[string]string `json:"custom_headers"`
32+
ProxyPass string `json:"proxy_pass"`
33+
SSL SSLConfig `json:"ssl"`
34+
RequestTimeout int `json:"request_timeout"` // in seconds
35+
ReadHeaderTimeout int `json:"read_header_timeout"` // in seconds
36+
IdleTimeout int `json:"idle_timeout"` // in seconds
37+
MaxHeaderBytes int `json:"max_header_bytes"` // in bytes
38+
FlushInterval string `json:"proxy_flush_interval"`
39+
BufferSizeKB int `json:"buffer_size_kb"`
40+
EnableLogging *bool `json:"enable_logging,omitempty"` // Default true if nil
3741

3842
PluginConfigs map[string]interface{} `json:"plugin_configs"`
3943
}

internal/server/handler.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ func createHandler(conf config.SiteConfig, log *logger.Logger, identifier string
6060
siteMwManager.Use(middleware.TimeoutMiddleware(timeout))
6161
}
6262

63-
// Add logging middleware last to ensure it wraps the entire request
64-
siteMwManager.Use(middleware.LoggingMiddleware(log, conf.Domain, identifier))
63+
// Add logging middleware last to ensure it wraps the entire request.
64+
// We default to true if the pointer is nil.
65+
if conf.EnableLogging == nil || *conf.EnableLogging {
66+
siteMwManager.Use(middleware.LoggingMiddleware(log, conf.Domain, identifier))
67+
}
6568

6669
// Apply the final chain of middleware
6770
handler = siteMwManager.Apply(handler)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package middleware
2+
3+
import (
4+
"sync"
5+
6+
"github.com/mirkobrombin/goup/internal/logger"
7+
"github.com/mirkobrombin/goup/internal/tui"
8+
)
9+
10+
// LogEntry represents a single log event.
11+
type LogEntry struct {
12+
Logger *logger.Logger
13+
Fields logger.Fields
14+
Message string
15+
Identifier string
16+
}
17+
18+
// AsyncLogger handles logging asynchronously.
19+
type AsyncLogger struct {
20+
logChan chan *LogEntry
21+
logEntryPool sync.Pool
22+
}
23+
24+
var globalAsyncLogger *AsyncLogger
25+
26+
// InitAsyncLogger initializes the global async logger.
27+
func InitAsyncLogger(bufferSize int) {
28+
globalAsyncLogger = &AsyncLogger{
29+
logChan: make(chan *LogEntry, bufferSize),
30+
logEntryPool: sync.Pool{
31+
New: func() interface{} {
32+
return &LogEntry{
33+
Fields: make(logger.Fields),
34+
}
35+
},
36+
},
37+
}
38+
go globalAsyncLogger.worker()
39+
}
40+
41+
// GetAsyncLogger returns the global async logger instance.
42+
func GetAsyncLogger() *AsyncLogger {
43+
return globalAsyncLogger
44+
}
45+
46+
// GetEntry retrieves a LogEntry from the pool.
47+
func (al *AsyncLogger) GetEntry() *LogEntry {
48+
return al.logEntryPool.Get().(*LogEntry)
49+
}
50+
51+
// Log queues a log entry. If the buffer is full, the log is dropped
52+
// and the entry is returned to the pool immediately.
53+
func (al *AsyncLogger) Log(entry *LogEntry) {
54+
select {
55+
case al.logChan <- entry:
56+
default:
57+
// Drop log if buffer is full to maintain performance
58+
al.PutEntry(entry)
59+
}
60+
}
61+
62+
// PutEntry resets and returns a LogEntry to the pool.
63+
func (al *AsyncLogger) PutEntry(entry *LogEntry) {
64+
entry.Logger = nil
65+
entry.Message = ""
66+
entry.Identifier = ""
67+
for k := range entry.Fields {
68+
delete(entry.Fields, k)
69+
}
70+
al.logEntryPool.Put(entry)
71+
}
72+
73+
// worker processes log entries from the channel.
74+
func (al *AsyncLogger) worker() {
75+
for entry := range al.logChan {
76+
entry.Logger.WithFields(entry.Fields).Info(entry.Message)
77+
78+
if tui.IsEnabled() {
79+
tui.UpdateLog(entry.Identifier, entry.Fields)
80+
}
81+
82+
al.PutEntry(entry)
83+
}
84+
}
85+
86+
// Shutdown closes the log channel.
87+
func (al *AsyncLogger) Shutdown() {
88+
close(al.logChan)
89+
}

internal/server/middleware/middleware.go

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package middleware
22

33
import (
4+
"bufio"
45
"fmt"
6+
"io"
7+
"net"
58
"net/http"
9+
"sync"
610
"time"
711

812
"github.com/mirkobrombin/goup/internal/logger"
@@ -11,11 +15,21 @@ import (
1115

1216
// LoggingMiddleware logs HTTP requests.
1317
func LoggingMiddleware(l *logger.Logger, domain string, identifier string) MiddlewareFunc {
18+
// sync.Pool for responseWriter to reduce allocation (Operation "31")
19+
rwPool := sync.Pool{
20+
New: func() interface{} {
21+
return &responseWriter{}
22+
},
23+
}
24+
1425
return func(next http.Handler) http.Handler {
1526
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1627
start := time.Now()
1728

18-
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
29+
// Get ResponseWriter from pool
30+
rw := rwPool.Get().(*responseWriter)
31+
rw.ResponseWriter = w
32+
rw.statusCode = http.StatusOK
1933

2034
next.ServeHTTP(rw, r)
2135

@@ -29,19 +43,38 @@ func LoggingMiddleware(l *logger.Logger, domain string, identifier string) Middl
2943
remoteAddr = ips
3044
}
3145

32-
fields := logger.Fields{
33-
"method": r.Method,
34-
"url": r.URL.String(),
35-
"remote_addr": remoteAddr,
36-
"status_code": rw.statusCode,
37-
"duration_sec": duration.Seconds(),
38-
"domain": domain,
46+
// Use Async Logging if initialized
47+
if asyncLog := GetAsyncLogger(); asyncLog != nil {
48+
entry := asyncLog.GetEntry()
49+
entry.Logger = l
50+
entry.Message = "Handled request"
51+
entry.Identifier = identifier
52+
entry.Fields["method"] = r.Method
53+
entry.Fields["url"] = r.URL.String()
54+
entry.Fields["remote_addr"] = remoteAddr
55+
entry.Fields["status_code"] = rw.statusCode
56+
entry.Fields["duration_sec"] = duration.Seconds()
57+
entry.Fields["domain"] = domain
58+
59+
asyncLog.Log(entry)
60+
} else {
61+
// Fallback to sync logging (creates allocations)
62+
fields := logger.Fields{
63+
"method": r.Method,
64+
"url": r.URL.String(),
65+
"remote_addr": remoteAddr,
66+
"status_code": rw.statusCode,
67+
"duration_sec": duration.Seconds(),
68+
"domain": domain,
69+
}
70+
l.WithFields(fields).Info("Handled request")
71+
if tui.IsEnabled() {
72+
tui.UpdateLog(identifier, fields)
73+
}
3974
}
40-
l.WithFields(fields).Info("Handled request")
4175

42-
if tui.IsEnabled() {
43-
tui.UpdateLog(identifier, fields)
44-
}
76+
rw.ResponseWriter = nil
77+
rwPool.Put(rw)
4578
})
4679
}
4780
}
@@ -91,3 +124,34 @@ func (rw *responseWriter) WriteHeader(code int) {
91124
rw.statusCode = code
92125
rw.ResponseWriter.WriteHeader(code)
93126
}
127+
128+
// Flush implements http.Flusher.
129+
func (rw *responseWriter) Flush() {
130+
if flusher, ok := rw.ResponseWriter.(http.Flusher); ok {
131+
flusher.Flush()
132+
}
133+
}
134+
135+
// Hijack implements http.Hijacker.
136+
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
137+
if hijacker, ok := rw.ResponseWriter.(http.Hijacker); ok {
138+
return hijacker.Hijack()
139+
}
140+
return nil, nil, http.ErrNotSupported
141+
}
142+
143+
// ReadFrom implements io.ReaderFrom.
144+
func (rw *responseWriter) ReadFrom(r io.Reader) (n int64, err error) {
145+
if rf, ok := rw.ResponseWriter.(io.ReaderFrom); ok {
146+
return rf.ReadFrom(r)
147+
}
148+
return io.Copy(rw.ResponseWriter, r)
149+
}
150+
151+
// Push implements http.Pusher.
152+
func (rw *responseWriter) Push(target string, opts *http.PushOptions) error {
153+
if pusher, ok := rw.ResponseWriter.(http.Pusher); ok {
154+
return pusher.Push(target, opts)
155+
}
156+
return http.ErrNotSupported
157+
}

internal/server/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ func StartServers(configs []config.SiteConfig, enableTUI bool, enableBench bool)
3333
tui.InitTUI()
3434
}
3535

36+
// Initialize the global async logger
37+
middleware.InitAsyncLogger(10000)
38+
3639
// Start API Server if enabled
3740
api.StartAPIServer()
3841

internal/server/server_utils.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,64 @@
11
package server
22

33
import (
4+
"context"
45
"crypto/tls"
56
"fmt"
7+
"net"
68
"net/http"
79
"os"
10+
"syscall"
811
"time"
912

1013
"github.com/mirkobrombin/goup/internal/config"
1114
"github.com/mirkobrombin/goup/internal/logger"
1215
"github.com/quic-go/quic-go/http3"
16+
"golang.org/x/sys/unix"
1317
)
1418

1519
// createHTTPServer creates an HTTP server with the given configuration and handler.
1620
func createHTTPServer(conf config.SiteConfig, handler http.Handler) *http.Server {
17-
return &http.Server{
21+
readTimeout := time.Duration(0)
22+
writeTimeout := time.Duration(0)
23+
if conf.RequestTimeout >= 0 {
24+
readTimeout = time.Duration(conf.RequestTimeout) * time.Second
25+
writeTimeout = time.Duration(conf.RequestTimeout) * time.Second
26+
}
27+
28+
s := &http.Server{
1829
Addr: fmt.Sprintf(":%d", conf.Port),
1930
Handler: handler,
20-
ReadTimeout: time.Duration(conf.RequestTimeout) * time.Second,
21-
WriteTimeout: time.Duration(conf.RequestTimeout) * time.Second,
31+
ReadTimeout: readTimeout,
32+
WriteTimeout: writeTimeout,
2233
TLSConfig: &tls.Config{
2334
NextProtos: []string{"h3", "h2", "http/1.1"},
2435
},
2536
}
37+
38+
if conf.ReadHeaderTimeout > 0 {
39+
s.ReadHeaderTimeout = time.Duration(conf.ReadHeaderTimeout) * time.Second
40+
}
41+
if conf.IdleTimeout > 0 {
42+
s.IdleTimeout = time.Duration(conf.IdleTimeout) * time.Second
43+
}
44+
if conf.MaxHeaderBytes > 0 {
45+
s.MaxHeaderBytes = conf.MaxHeaderBytes
46+
}
47+
48+
return s
49+
}
50+
51+
// listenOptimized creates a TCP listener with SO_REUSEPORT and TCP_FASTOPEN optimizations.
52+
func listenOptimized(addr string) (net.Listener, error) {
53+
lc := net.ListenConfig{
54+
Control: func(network, address string, c syscall.RawConn) error {
55+
return c.Control(func(fd uintptr) {
56+
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
57+
unix.SetsockoptInt(int(fd), unix.SOL_TCP, unix.TCP_FASTOPEN, 256)
58+
})
59+
},
60+
}
61+
return lc.Listen(context.Background(), "tcp", addr)
2662
}
2763

2864
// startServerInstance starts the HTTP server instance.
@@ -56,7 +92,12 @@ func startServerInstance(server *http.Server, conf config.SiteConfig, l *logger.
5692
}
5793
} else {
5894
l.Infof("Serving on HTTP port %d", conf.Port)
59-
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
95+
ln, err := listenOptimized(server.Addr)
96+
if err != nil {
97+
l.Errorf("Error listening on port %d: %v", conf.Port, err)
98+
return
99+
}
100+
if err := server.Serve(ln); err != nil && err != http.ErrServerClosed {
60101
l.Errorf("Server error on port %d: %v", conf.Port, err)
61102
}
62103
}

0 commit comments

Comments
 (0)