mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 19:12:54 +00:00
- Add ETagData struct with Path/Method/Content/Timestamp/UserID/Params fields for ETag generation - Add ETagGenerator struct with secretKey/cacheStorage fields and NewETagGenerator constructor - Add GenerateETag method with SHA-256 hashing, path/method/content/timestamp/userID/params/secretKey composition - Add GenerateWeakETag for dynamic content, GetCachedETag/CacheETag for cache operations,
581 lines
16 KiB
Go
581 lines
16 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// PerformanceMetrics tracks performance statistics
|
|
type PerformanceMetrics struct {
|
|
mu sync.RWMutex
|
|
requestCount int64
|
|
totalResponseTime time.Duration
|
|
slowRequests int64
|
|
fastRequests int64
|
|
errorRequests int64
|
|
}
|
|
|
|
// PerformanceMiddleware tracks and optimizes API performance
|
|
type PerformanceMiddleware struct {
|
|
metrics *PerformanceMetrics
|
|
slowThreshold time.Duration
|
|
}
|
|
|
|
// NewPerformanceMiddleware creates a new performance middleware
|
|
func NewPerformanceMiddleware(slowThreshold time.Duration) *PerformanceMiddleware {
|
|
return &PerformanceMiddleware{
|
|
metrics: &PerformanceMetrics{},
|
|
slowThreshold: slowThreshold,
|
|
}
|
|
}
|
|
|
|
// Middleware returns the gin.HandlerFunc
|
|
func (pm *PerformanceMiddleware) Middleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
|
|
// Process request
|
|
c.Next()
|
|
|
|
// Calculate response time
|
|
responseTime := time.Since(start)
|
|
|
|
// Update metrics
|
|
pm.updateMetrics(c.Writer.Status(), responseTime)
|
|
|
|
// Add performance headers
|
|
c.Header("X-Response-Time", responseTime.String())
|
|
c.Header("X-Request-ID", c.GetHeader("X-Request-ID"))
|
|
|
|
// Log slow requests
|
|
if responseTime > pm.slowThreshold {
|
|
pm.logSlowRequest(c, responseTime)
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateMetrics updates the performance metrics
|
|
func (pm *PerformanceMiddleware) updateMetrics(statusCode int, responseTime time.Duration) {
|
|
pm.metrics.mu.Lock()
|
|
defer pm.metrics.mu.Unlock()
|
|
|
|
pm.metrics.requestCount++
|
|
pm.metrics.totalResponseTime += responseTime
|
|
|
|
if responseTime > pm.slowThreshold {
|
|
pm.metrics.slowRequests++
|
|
} else {
|
|
pm.metrics.fastRequests++
|
|
}
|
|
|
|
if statusCode >= 400 {
|
|
pm.metrics.errorRequests++
|
|
}
|
|
}
|
|
|
|
// logSlowRequest logs slow requests for monitoring
|
|
func (pm *PerformanceMiddleware) logSlowRequest(c *gin.Context, responseTime time.Duration) {
|
|
// In production, this would go to a structured logging system
|
|
// For now, we'll use the standard logger
|
|
gin.DefaultWriter.Write(fmt.Appendf(nil,
|
|
"[SLOW REQUEST] %s %s - %v - Status: %d\n",
|
|
c.Request.Method,
|
|
c.Request.URL.Path,
|
|
responseTime,
|
|
c.Writer.Status(),
|
|
))
|
|
}
|
|
|
|
// GetMetrics returns current performance metrics
|
|
func (pm *PerformanceMiddleware) GetMetrics() PerformanceMetrics {
|
|
pm.metrics.mu.RLock()
|
|
defer pm.metrics.mu.RUnlock()
|
|
|
|
return PerformanceMetrics{
|
|
requestCount: pm.metrics.requestCount,
|
|
totalResponseTime: pm.metrics.totalResponseTime,
|
|
slowRequests: pm.metrics.slowRequests,
|
|
fastRequests: pm.metrics.fastRequests,
|
|
errorRequests: pm.metrics.errorRequests,
|
|
}
|
|
}
|
|
|
|
// GetAverageResponseTime returns the average response time
|
|
func (pm *PerformanceMiddleware) GetAverageResponseTime() time.Duration {
|
|
pm.metrics.mu.RLock()
|
|
defer pm.metrics.mu.RUnlock()
|
|
|
|
if pm.metrics.requestCount == 0 {
|
|
return 0
|
|
}
|
|
|
|
return pm.metrics.totalResponseTime / time.Duration(pm.metrics.requestCount)
|
|
}
|
|
|
|
// GetSlowRequestPercentage returns the percentage of slow requests
|
|
func (pm *PerformanceMiddleware) GetSlowRequestPercentage() float64 {
|
|
pm.metrics.mu.RLock()
|
|
defer pm.metrics.mu.RUnlock()
|
|
|
|
if pm.metrics.requestCount == 0 {
|
|
return 0
|
|
}
|
|
|
|
return float64(pm.metrics.slowRequests) / float64(pm.metrics.requestCount) * 100
|
|
}
|
|
|
|
// GetErrorRate returns the error rate percentage
|
|
func (pm *PerformanceMiddleware) GetErrorRate() float64 {
|
|
pm.metrics.mu.RLock()
|
|
defer pm.metrics.mu.RUnlock()
|
|
|
|
if pm.metrics.requestCount == 0 {
|
|
return 0
|
|
}
|
|
|
|
return float64(pm.metrics.errorRequests) / float64(pm.metrics.requestCount) * 100
|
|
}
|
|
|
|
// ResetMetrics resets all performance metrics
|
|
func (pm *PerformanceMiddleware) ResetMetrics() {
|
|
pm.metrics.mu.Lock()
|
|
defer pm.metrics.mu.Unlock()
|
|
|
|
pm.metrics.requestCount = 0
|
|
pm.metrics.totalResponseTime = 0
|
|
pm.metrics.slowRequests = 0
|
|
pm.metrics.fastRequests = 0
|
|
pm.metrics.errorRequests = 0
|
|
}
|
|
|
|
// Global performance middleware instance
|
|
var globalPerformanceMiddleware *PerformanceMiddleware
|
|
|
|
// InitializePerformanceMiddleware initializes the global performance middleware
|
|
func InitializePerformanceMiddleware(slowThreshold time.Duration) {
|
|
globalPerformanceMiddleware = NewPerformanceMiddleware(slowThreshold)
|
|
}
|
|
|
|
// GetPerformanceMiddleware returns the global performance middleware
|
|
func GetPerformanceMiddleware() *PerformanceMiddleware {
|
|
return globalPerformanceMiddleware
|
|
}
|
|
|
|
// PerformanceMiddlewareHandler returns a gin.HandlerFunc for performance tracking
|
|
func PerformanceMiddlewareHandler() gin.HandlerFunc {
|
|
if globalPerformanceMiddleware == nil {
|
|
// Initialize with default threshold if not already initialized
|
|
InitializePerformanceMiddleware(100 * time.Millisecond)
|
|
}
|
|
return globalPerformanceMiddleware.Middleware()
|
|
}
|
|
|
|
// PerformanceHandler returns performance metrics as JSON
|
|
func PerformanceHandler() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if globalPerformanceMiddleware == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "Performance middleware not initialized",
|
|
})
|
|
return
|
|
}
|
|
|
|
metrics := globalPerformanceMiddleware.GetMetrics()
|
|
avgResponseTime := globalPerformanceMiddleware.GetAverageResponseTime()
|
|
slowRequestPercentage := globalPerformanceMiddleware.GetSlowRequestPercentage()
|
|
errorRate := globalPerformanceMiddleware.GetErrorRate()
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"metrics": gin.H{
|
|
"request_count": metrics.requestCount,
|
|
"total_response_time": metrics.totalResponseTime.String(),
|
|
"slow_requests": metrics.slowRequests,
|
|
"fast_requests": metrics.fastRequests,
|
|
"error_requests": metrics.errorRequests,
|
|
},
|
|
"performance": gin.H{
|
|
"average_response_time_ms": avgResponseTime.Milliseconds(),
|
|
"slow_request_percentage": slowRequestPercentage,
|
|
"error_rate_percentage": errorRate,
|
|
"slow_threshold_ms": globalPerformanceMiddleware.slowThreshold.Milliseconds(),
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// RequestSizeMiddleware limits request size for performance
|
|
func RequestSizeMiddleware(maxSize int64) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// CompressionMiddleware enables gzip compression for better performance
|
|
func CompressionMiddleware() gin.HandlerFunc {
|
|
// Use gin's built-in compression
|
|
return func(c *gin.Context) {
|
|
c.Header("Content-Encoding", "gzip")
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// CacheMiddleware adds basic caching headers for GET requests
|
|
func CacheMiddleware(duration time.Duration) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if c.Request.Method == "GET" {
|
|
c.Header("Cache-Control", "public, max-age="+duration.String())
|
|
c.Header("ETag", generateETag(c))
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ETagData represents the components used for ETag generation
|
|
type ETagData struct {
|
|
Path string
|
|
Method string
|
|
Content []byte
|
|
Timestamp time.Time
|
|
UserID string
|
|
Params string
|
|
}
|
|
|
|
// ETagGenerator handles sophisticated ETag generation
|
|
type ETagGenerator struct {
|
|
secretKey []byte
|
|
cacheStorage sync.Map // For storing computed ETags
|
|
}
|
|
|
|
// NewETagGenerator creates a new ETag generator instance
|
|
func NewETagGenerator(secretKey string) *ETagGenerator {
|
|
return &ETagGenerator{
|
|
secretKey: []byte(secretKey),
|
|
}
|
|
}
|
|
|
|
// GenerateETag creates a sophisticated ETag based on multiple factors
|
|
func (g *ETagGenerator) GenerateETag(data ETagData) string {
|
|
// Create a composite hash from multiple factors
|
|
h := sha256.New()
|
|
|
|
// Include path and method
|
|
h.Write([]byte(data.Path))
|
|
h.Write([]byte(data.Method))
|
|
|
|
// Include content if available
|
|
if len(data.Content) > 0 {
|
|
h.Write(data.Content)
|
|
}
|
|
|
|
// Include timestamp (rounded to second for consistency)
|
|
timestampBytes := []byte(fmt.Sprintf("%d", data.Timestamp.Unix()))
|
|
h.Write(timestampBytes)
|
|
|
|
// Include user context for personalized responses
|
|
if data.UserID != "" {
|
|
h.Write([]byte(data.UserID))
|
|
}
|
|
|
|
// Include query parameters for cache differentiation
|
|
if data.Params != "" {
|
|
h.Write([]byte(data.Params))
|
|
}
|
|
|
|
// Add secret key for security
|
|
h.Write(g.secretKey)
|
|
|
|
// Generate final ETag
|
|
hash := h.Sum(nil)
|
|
return fmt.Sprintf("\"%x\"", hash)
|
|
}
|
|
|
|
// GenerateWeakETag creates a weak ETag for dynamic content
|
|
func (g *ETagGenerator) GenerateWeakETag(data ETagData) string {
|
|
etag := g.GenerateETag(data)
|
|
return "W/" + etag
|
|
}
|
|
|
|
// GetCachedETag retrieves a cached ETag if available
|
|
func (g *ETagGenerator) GetCachedETag(key string) (string, bool) {
|
|
if cached, ok := g.cacheStorage.Load(key); ok {
|
|
if etag, ok := cached.(string); ok {
|
|
return etag, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// CacheETag stores an ETag in the cache
|
|
func (g *ETagGenerator) CacheETag(key, etag string) {
|
|
g.cacheStorage.Store(key, etag)
|
|
}
|
|
|
|
// InvalidateCache removes ETag entries matching a pattern
|
|
func (g *ETagGenerator) InvalidateCache(pattern string) {
|
|
g.cacheStorage.Range(func(key, value interface{}) bool {
|
|
if keyStr, ok := key.(string); ok {
|
|
if strings.Contains(keyStr, pattern) {
|
|
g.cacheStorage.Delete(key)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// Global ETag generator instance
|
|
var etagGenerator = NewETagGenerator("telecom-platform-etag-secret")
|
|
|
|
// generateETag generates a sophisticated ETag for caching
|
|
func generateETag(c *gin.Context) string {
|
|
// Extract relevant data for ETag generation
|
|
data := ETagData{
|
|
Path: c.Request.URL.Path,
|
|
Method: c.Request.Method,
|
|
Timestamp: time.Now(),
|
|
Params: c.Request.URL.RawQuery,
|
|
}
|
|
|
|
// Add user context if available
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
if idStr, ok := userID.(string); ok {
|
|
data.UserID = idStr
|
|
}
|
|
}
|
|
|
|
// Try to get cached ETag first
|
|
cacheKey := fmt.Sprintf("%s:%s:%s", data.Method, data.Path, data.Params)
|
|
if cached, exists := etagGenerator.GetCachedETag(cacheKey); exists {
|
|
return cached
|
|
}
|
|
|
|
// Generate new ETag
|
|
etag := etagGenerator.GenerateETag(data)
|
|
|
|
// Cache the ETag for future use
|
|
etagGenerator.CacheETag(cacheKey, etag)
|
|
|
|
return etag
|
|
}
|
|
|
|
// generateContentETag creates an ETag based on response content
|
|
func generateContentETag(c *gin.Context, content []byte) string {
|
|
data := ETagData{
|
|
Path: c.Request.URL.Path,
|
|
Method: c.Request.Method,
|
|
Content: content,
|
|
Timestamp: time.Now(),
|
|
Params: c.Request.URL.RawQuery,
|
|
}
|
|
|
|
// Add user context if available
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
if idStr, ok := userID.(string); ok {
|
|
data.UserID = idStr
|
|
}
|
|
}
|
|
|
|
return etagGenerator.GenerateETag(data)
|
|
}
|
|
|
|
// generateWeakETag creates a weak ETag for dynamic content
|
|
func generateWeakETag(c *gin.Context) string {
|
|
data := ETagData{
|
|
Path: c.Request.URL.Path,
|
|
Method: c.Request.Method,
|
|
Timestamp: time.Now(),
|
|
Params: c.Request.URL.RawQuery,
|
|
}
|
|
|
|
// Add user context if available
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
if idStr, ok := userID.(string); ok {
|
|
data.UserID = idStr
|
|
}
|
|
}
|
|
|
|
return etagGenerator.GenerateWeakETag(data)
|
|
}
|
|
|
|
// ETagValidationMiddleware handles ETag validation for conditional requests
|
|
func ETagValidationMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Only handle GET and HEAD requests for ETag validation
|
|
if c.Request.Method != "GET" && c.Request.Method != "HEAD" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Check If-None-Match header (for GET requests)
|
|
if noneMatch := c.GetHeader("If-None-Match"); noneMatch != "" {
|
|
// Generate current ETag
|
|
currentETag := generateETag(c)
|
|
|
|
// Check if any of the ETags match
|
|
if etagMatches(noneMatch, currentETag) {
|
|
c.Header("ETag", currentETag)
|
|
c.Status(http.StatusNotModified)
|
|
c.Abort()
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check If-Match header (for PUT/PATCH/DELETE requests)
|
|
if match := c.GetHeader("If-Match"); match != "" {
|
|
currentETag := generateETag(c)
|
|
|
|
if !etagMatches(match, currentETag) {
|
|
c.Status(http.StatusPreconditionFailed)
|
|
c.Abort()
|
|
return
|
|
}
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// ContentBasedETagMiddleware generates ETags based on response content
|
|
func ContentBasedETagMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Next()
|
|
|
|
// Only generate ETags for successful GET requests
|
|
if c.Request.Method != "GET" || c.Writer.Status() != http.StatusOK {
|
|
return
|
|
}
|
|
|
|
// For content-based ETags, we would need to capture the response body
|
|
// This is a simplified version that uses weak ETags for dynamic content
|
|
etag := generateWeakETag(c)
|
|
c.Header("ETag", etag)
|
|
}
|
|
}
|
|
|
|
// CacheInvalidationMiddleware handles cache invalidation for modified resources
|
|
func CacheInvalidationMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Store original request for post-processing
|
|
c.Next()
|
|
|
|
// Invalidate cache for POST, PUT, PATCH, DELETE requests
|
|
if c.Request.Method == "POST" || c.Request.Method == "PUT" ||
|
|
c.Request.Method == "PATCH" || c.Request.Method == "DELETE" {
|
|
|
|
// Invalidate ETags for this resource pattern
|
|
pattern := c.Request.URL.Path
|
|
etagGenerator.InvalidateCache(pattern)
|
|
|
|
// Also invalidate related collection resources
|
|
if strings.Contains(pattern, "/") {
|
|
parts := strings.Split(strings.Trim(pattern, "/"), "/")
|
|
if len(parts) > 0 {
|
|
collectionPattern := "/" + parts[0]
|
|
etagGenerator.InvalidateCache(collectionPattern)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// etagMatches checks if any of the provided ETags match the current ETag
|
|
func etagMatches(headerValue, currentETag string) bool {
|
|
// Handle wildcard
|
|
if headerValue == "*" {
|
|
return true
|
|
}
|
|
|
|
// Parse the header value (can contain multiple ETags)
|
|
etags := strings.Split(headerValue, ",")
|
|
for _, etag := range etags {
|
|
etag = strings.TrimSpace(etag)
|
|
// Remove quotes for comparison
|
|
etag = strings.Trim(etag, "\"")
|
|
current := strings.Trim(currentETag, "\"W/")
|
|
|
|
if etag == current || etag == "W/"+current {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// AdvancedCacheMiddleware provides sophisticated caching with ETags
|
|
func AdvancedCacheMiddleware(maxAge time.Duration, useWeakETag bool) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if c.Request.Method == "GET" || c.Request.Method == "HEAD" {
|
|
// Set cache control headers
|
|
cacheControl := fmt.Sprintf("public, max-age=%d", int(maxAge.Seconds()))
|
|
if useWeakETag {
|
|
cacheControl += ", must-revalidate"
|
|
}
|
|
c.Header("Cache-Control", cacheControl)
|
|
|
|
// Generate appropriate ETag
|
|
var etag string
|
|
if useWeakETag {
|
|
etag = generateWeakETag(c)
|
|
} else {
|
|
etag = generateETag(c)
|
|
}
|
|
c.Header("ETag", etag)
|
|
|
|
// Add Vary header for proper caching with different user contexts
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
c.Header("Vary", "Accept, Authorization")
|
|
if userIDStr, ok := userID.(string); ok && userIDStr != "" {
|
|
c.Header("X-User-Context", userIDStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// TimeoutMiddleware adds timeout to requests
|
|
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
|
|
defer cancel()
|
|
c.Request = c.Request.WithContext(ctx)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// SecurityHeadersMiddleware adds security headers for better performance
|
|
func SecurityHeadersMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
c.Header("X-Frame-Options", "DENY")
|
|
c.Header("X-XSS-Protection", "1; mode=block")
|
|
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// CORSMiddleware handles CORS for better performance
|
|
func CORSMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
c.Header("Access-Control-Max-Age", "86400")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|