Add SQL query logging and performance improvements
- Implement comprehensive SQL query logging for queries over 10ms - Add logging wrapper methods for all database operations - Replace timing code in GetStats with simple info log messages - Add missing database indexes for better query performance: - idx_live_routes_lookup for common prefix/origin/peer lookups - idx_live_routes_withdraw for withdrawal updates - idx_prefixes_prefix for prefix lookups - idx_asn_peerings_lookup for peering relationship queries - Increase SQLite cache size to 512MB - Add performance-oriented SQLite pragmas - Extract HTML templates to separate files using go:embed - Add JSON response middleware with @meta field (like bgpview.io API) - Fix concurrent map write errors in HTTP handlers - Add request timeout handling with proper JSON error responses These changes significantly improve database query performance and provide visibility into slow queries for debugging purposes.
This commit is contained in:
201
internal/server/middleware.go
Normal file
201
internal/server/middleware.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// responseWriter wraps http.ResponseWriter to capture the response
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
statusCode int
|
||||
written bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
rw.mu.Lock()
|
||||
defer rw.mu.Unlock()
|
||||
|
||||
if !rw.written {
|
||||
rw.written = true
|
||||
}
|
||||
|
||||
return rw.body.Write(b)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(statusCode int) {
|
||||
rw.mu.Lock()
|
||||
defer rw.mu.Unlock()
|
||||
|
||||
if !rw.written {
|
||||
rw.statusCode = statusCode
|
||||
rw.written = true
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Header() http.Header {
|
||||
return rw.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
// JSONResponseMiddleware wraps all JSON responses with metadata
|
||||
func JSONResponseMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip non-JSON endpoints
|
||||
if r.URL.Path == "/" || r.URL.Path == "/status" {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a custom response writer to capture the response
|
||||
rw := &responseWriter{
|
||||
ResponseWriter: w,
|
||||
body: &bytes.Buffer{},
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
// Serve the request
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
// Calculate execution time
|
||||
execTime := time.Since(startTime)
|
||||
|
||||
// Only process JSON responses
|
||||
contentType := rw.Header().Get("Content-Type")
|
||||
if contentType != "application/json" || rw.body.Len() == 0 {
|
||||
// Write the original response
|
||||
w.WriteHeader(rw.statusCode)
|
||||
_, _ = w.Write(rw.body.Bytes())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the original response
|
||||
var originalResponse map[string]interface{}
|
||||
if err := json.Unmarshal(rw.body.Bytes(), &originalResponse); err != nil {
|
||||
// If we can't parse it, just write original
|
||||
w.WriteHeader(rw.statusCode)
|
||||
_, _ = w.Write(rw.body.Bytes())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Add @meta field
|
||||
originalResponse["@meta"] = map[string]interface{}{
|
||||
"time_zone": "UTC",
|
||||
"api_version": 1,
|
||||
"execution_time": fmt.Sprintf("%d ms", execTime.Milliseconds()),
|
||||
}
|
||||
|
||||
// Write the enhanced response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(rw.statusCode)
|
||||
_ = json.NewEncoder(w).Encode(originalResponse)
|
||||
})
|
||||
}
|
||||
|
||||
// timeoutWriter wraps ResponseWriter to prevent concurrent writes after timeout
|
||||
type timeoutWriter struct {
|
||||
http.ResponseWriter
|
||||
mu sync.Mutex
|
||||
written bool
|
||||
}
|
||||
|
||||
func (tw *timeoutWriter) Write(b []byte) (int, error) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
|
||||
if tw.written {
|
||||
return 0, nil // Discard writes after timeout
|
||||
}
|
||||
|
||||
return tw.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (tw *timeoutWriter) WriteHeader(statusCode int) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
|
||||
if tw.written {
|
||||
return // Discard writes after timeout
|
||||
}
|
||||
|
||||
tw.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (tw *timeoutWriter) Header() http.Header {
|
||||
return tw.ResponseWriter.Header()
|
||||
}
|
||||
|
||||
func (tw *timeoutWriter) markWritten() {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.written = true
|
||||
}
|
||||
|
||||
// TimeoutMiddleware creates a timeout middleware that returns JSON errors
|
||||
func TimeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), timeout)
|
||||
defer cancel()
|
||||
|
||||
tw := &timeoutWriter{
|
||||
ResponseWriter: w,
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
panicChan := make(chan interface{}, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicChan <- p
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(tw, r.WithContext(ctx))
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case p := <-panicChan:
|
||||
panic(p)
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
tw.markWritten() // Prevent the handler from writing after timeout
|
||||
execTime := time.Since(startTime)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusRequestTimeout)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": map[string]interface{}{
|
||||
"msg": "Request timeout",
|
||||
"code": http.StatusRequestTimeout,
|
||||
},
|
||||
"@meta": map[string]interface{}{
|
||||
"time_zone": "UTC",
|
||||
"api_version": 1,
|
||||
"execution_time": fmt.Sprintf("%d ms", execTime.Milliseconds()),
|
||||
},
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,9 @@ func (s *Server) setupRoutes() {
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
const requestTimeout = 60 * time.Second
|
||||
r.Use(middleware.Timeout(requestTimeout))
|
||||
const requestTimeout = 2 * time.Second
|
||||
r.Use(TimeoutMiddleware(requestTimeout))
|
||||
r.Use(JSONResponseMiddleware)
|
||||
|
||||
// Routes
|
||||
r.Get("/", s.handleRoot())
|
||||
@@ -124,15 +125,60 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
|
||||
LiveRoutes int `json:"live_routes"`
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a 1 second timeout context for this request
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
metrics := s.streamer.GetMetrics()
|
||||
|
||||
// Get database stats
|
||||
dbStats, err := s.db.GetStats()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// Get database stats with timeout
|
||||
statsChan := make(chan database.Stats)
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
s.logger.Debug("Starting database stats query")
|
||||
dbStats, err := s.db.GetStats()
|
||||
if err != nil {
|
||||
s.logger.Debug("Database stats query failed", "error", err)
|
||||
errChan <- err
|
||||
|
||||
return
|
||||
}
|
||||
s.logger.Debug("Database stats query completed")
|
||||
statsChan <- dbStats
|
||||
}()
|
||||
|
||||
var dbStats database.Stats
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.logger.Error("Database stats timeout in status.json")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusRequestTimeout)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": map[string]interface{}{
|
||||
"msg": "Database timeout",
|
||||
"code": http.StatusRequestTimeout,
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
case err := <-errChan:
|
||||
s.logger.Error("Failed to get database stats", "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "error",
|
||||
"error": map[string]interface{}{
|
||||
"msg": err.Error(),
|
||||
"code": http.StatusInternalServerError,
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
case dbStats = <-statsChan:
|
||||
// Success
|
||||
}
|
||||
|
||||
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
||||
@@ -158,7 +204,11 @@ func (s *Server) handleStatusJSON() http.HandlerFunc {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
||||
response := map[string]interface{}{
|
||||
"status": "ok",
|
||||
"data": stats,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
s.logger.Error("Failed to encode stats", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -182,15 +232,53 @@ func (s *Server) handleStats() http.HandlerFunc {
|
||||
LiveRoutes int `json:"live_routes"`
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a 1 second timeout context for this request
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Check if context is already cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
http.Error(w, "Request timeout", http.StatusRequestTimeout)
|
||||
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
metrics := s.streamer.GetMetrics()
|
||||
|
||||
// Get database stats
|
||||
dbStats, err := s.db.GetStats()
|
||||
if err != nil {
|
||||
// Get database stats with timeout
|
||||
statsChan := make(chan database.Stats)
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
s.logger.Debug("Starting database stats query")
|
||||
dbStats, err := s.db.GetStats()
|
||||
if err != nil {
|
||||
s.logger.Debug("Database stats query failed", "error", err)
|
||||
errChan <- err
|
||||
|
||||
return
|
||||
}
|
||||
s.logger.Debug("Database stats query completed")
|
||||
statsChan <- dbStats
|
||||
}()
|
||||
|
||||
var dbStats database.Stats
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.logger.Error("Database stats timeout")
|
||||
http.Error(w, "Database timeout", http.StatusRequestTimeout)
|
||||
|
||||
return
|
||||
case err := <-errChan:
|
||||
s.logger.Error("Failed to get database stats", "error", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
case dbStats = <-statsChan:
|
||||
// Success
|
||||
}
|
||||
|
||||
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
||||
@@ -216,7 +304,11 @@ func (s *Server) handleStats() http.HandlerFunc {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
||||
response := map[string]interface{}{
|
||||
"status": "ok",
|
||||
"data": stats,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
s.logger.Error("Failed to encode stats", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user