落地结构化日志与基础监控告警
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Snapshot struct {
|
||||
GeneratedAtUTC string `json:"generated_at_utc"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
TotalRequests uint64 `json:"total_requests"`
|
||||
ClientErrors uint64 `json:"client_errors"`
|
||||
ServerErrors uint64 `json:"server_errors"`
|
||||
ClientErrorRatePct float64 `json:"client_error_rate_pct"`
|
||||
ServerErrorRatePct float64 `json:"server_error_rate_pct"`
|
||||
AvgLatencyMs float64 `json:"avg_latency_ms"`
|
||||
MaxLatencyMs float64 `json:"max_latency_ms"`
|
||||
}
|
||||
|
||||
type Collector struct {
|
||||
startedAt time.Time
|
||||
totalRequests uint64
|
||||
clientErrors uint64
|
||||
serverErrors uint64
|
||||
totalLatencyNs uint64
|
||||
maxLatencyNs uint64
|
||||
}
|
||||
|
||||
func NewCollector() *Collector {
|
||||
return &Collector{startedAt: time.Now()}
|
||||
}
|
||||
|
||||
func (c *Collector) Observe(status int, latency time.Duration) {
|
||||
atomic.AddUint64(&c.totalRequests, 1)
|
||||
atomic.AddUint64(&c.totalLatencyNs, uint64(latency.Nanoseconds()))
|
||||
|
||||
if status >= 500 {
|
||||
atomic.AddUint64(&c.serverErrors, 1)
|
||||
} else if status >= 400 {
|
||||
atomic.AddUint64(&c.clientErrors, 1)
|
||||
}
|
||||
|
||||
latNs := uint64(latency.Nanoseconds())
|
||||
for {
|
||||
old := atomic.LoadUint64(&c.maxLatencyNs)
|
||||
if latNs <= old {
|
||||
break
|
||||
}
|
||||
if atomic.CompareAndSwapUint64(&c.maxLatencyNs, old, latNs) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) Snapshot() Snapshot {
|
||||
now := time.Now()
|
||||
total := atomic.LoadUint64(&c.totalRequests)
|
||||
clientErr := atomic.LoadUint64(&c.clientErrors)
|
||||
serverErr := atomic.LoadUint64(&c.serverErrors)
|
||||
totalLatencyNs := atomic.LoadUint64(&c.totalLatencyNs)
|
||||
maxLatencyNs := atomic.LoadUint64(&c.maxLatencyNs)
|
||||
|
||||
var clientRate float64
|
||||
var serverRate float64
|
||||
var avgLatencyMs float64
|
||||
if total > 0 {
|
||||
clientRate = float64(clientErr) * 100 / float64(total)
|
||||
serverRate = float64(serverErr) * 100 / float64(total)
|
||||
avgLatencyMs = float64(totalLatencyNs) / float64(total) / 1e6
|
||||
}
|
||||
|
||||
return Snapshot{
|
||||
GeneratedAtUTC: now.UTC().Format(time.RFC3339),
|
||||
UptimeSeconds: int64(now.Sub(c.startedAt).Seconds()),
|
||||
TotalRequests: total,
|
||||
ClientErrors: clientErr,
|
||||
ServerErrors: serverErr,
|
||||
ClientErrorRatePct: clientRate,
|
||||
ServerErrorRatePct: serverRate,
|
||||
AvgLatencyMs: avgLatencyMs,
|
||||
MaxLatencyMs: float64(maxLatencyNs) / 1e6,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCollectorSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewCollector()
|
||||
c.Observe(200, 100*time.Millisecond)
|
||||
c.Observe(404, 300*time.Millisecond)
|
||||
c.Observe(500, 500*time.Millisecond)
|
||||
|
||||
s := c.Snapshot()
|
||||
if s.TotalRequests != 3 {
|
||||
t.Fatalf("total_requests=%d, want=3", s.TotalRequests)
|
||||
}
|
||||
if s.ClientErrors != 1 {
|
||||
t.Fatalf("client_errors=%d, want=1", s.ClientErrors)
|
||||
}
|
||||
if s.ServerErrors != 1 {
|
||||
t.Fatalf("server_errors=%d, want=1", s.ServerErrors)
|
||||
}
|
||||
if s.ServerErrorRatePct <= 0 {
|
||||
t.Fatalf("server_error_rate_pct=%f, want>0", s.ServerErrorRatePct)
|
||||
}
|
||||
if s.AvgLatencyMs <= 0 {
|
||||
t.Fatalf("avg_latency_ms=%f, want>0", s.AvgLatencyMs)
|
||||
}
|
||||
if s.MaxLatencyMs < 500 {
|
||||
t.Fatalf("max_latency_ms=%f, want>=500", s.MaxLatencyMs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"wx_service/internal/middleware"
|
||||
)
|
||||
|
||||
var requestSeq uint64
|
||||
|
||||
func RequestLogMiddleware(metrics *Collector) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
startedAt := time.Now()
|
||||
|
||||
requestID := strings.TrimSpace(c.GetHeader("X-Request-ID"))
|
||||
if requestID == "" {
|
||||
seq := atomic.AddUint64(&requestSeq, 1)
|
||||
requestID = fmt.Sprintf("req-%d-%d", time.Now().UnixNano(), seq)
|
||||
}
|
||||
c.Writer.Header().Set("X-Request-ID", requestID)
|
||||
|
||||
c.Next()
|
||||
|
||||
status := c.Writer.Status()
|
||||
latency := time.Since(startedAt)
|
||||
if metrics != nil {
|
||||
metrics.Observe(status, latency)
|
||||
}
|
||||
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
|
||||
level := "info"
|
||||
if status >= 500 {
|
||||
level = "error"
|
||||
} else if status >= 400 {
|
||||
level = "warn"
|
||||
}
|
||||
|
||||
entry := map[string]any{
|
||||
"ts": time.Now().UTC().Format(time.RFC3339),
|
||||
"level": level,
|
||||
"request_id": requestID,
|
||||
"method": c.Request.Method,
|
||||
"path": path,
|
||||
"status": status,
|
||||
"latency_ms": float64(latency.Microseconds()) / 1000.0,
|
||||
"client_ip": c.ClientIP(),
|
||||
}
|
||||
|
||||
if user, ok := middleware.CurrentUser(c); ok && user != nil {
|
||||
entry["uid"] = user.ID
|
||||
}
|
||||
if len(c.Errors) > 0 {
|
||||
entry["errors"] = c.Errors.String()
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
log.Printf("observability marshal log failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Println(string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func BasicMetricsHandler(metrics *Collector) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if metrics == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"code": http.StatusServiceUnavailable,
|
||||
"message": "metrics not initialized",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"message": "success",
|
||||
"data": metrics.Snapshot(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user