落地结构化日志与基础监控告警

This commit is contained in:
hello-dd-code
2026-02-28 16:37:37 +08:00
parent 78f488fbbb
commit 5666dc61a0
7 changed files with 345 additions and 0 deletions
+83
View File
@@ -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,
}
}
+35
View File
@@ -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)
}
}
+92
View File
@@ -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(),
})
}
}