Files
geo/server/internal/tenant/app/generation_observability.go
T
root 65e9b7e293
Deployment Config CI / Deployment Config (push) Successful in 24s
Backend CI / Backend (push) Successful in 14m12s
harden article generation reliability
2026-05-05 23:43:10 +08:00

241 lines
7.9 KiB
Go

package app
import (
"strings"
"sync/atomic"
"time"
"go.uber.org/zap"
)
const GenerationTaskStatusRunning = "running"
const (
GenerationTaskEventTransition = "transition"
GenerationTaskEventFailure = "failure"
GenerationTaskEventCleanupFailure = "cleanup_failure"
GenerationTaskEventQueueFailure = "queue_failure"
GenerationTaskEventRecoveryRequeue = "recovery_requeue"
GenerationTaskEventRecoveryFinalize = "recovery_finalize"
GenerationTaskEventRecoverySkipped = "recovery_skipped"
GenerationTaskEventStateAnomaly = "state_anomaly"
GenerationTaskEventStateCheckFailure = "state_check_failure"
)
const (
GenerationTaskResultSuccess = "success"
GenerationTaskResultFailure = "failure"
GenerationTaskResultSkipped = "skipped"
)
type GenerationTaskLogContext struct {
TaskID int64
TenantID int64
OperatorID int64
ArticleID int64
ReservationID int64
TaskType string
Stage string
AttemptCount int
LeaseOwner string
StatusBefore string
StatusAfter string
Result string
AnomalyType string
Severity string
Duration time.Duration
}
func GenerationTaskLogFields(ctx GenerationTaskLogContext) []zap.Field {
fields := []zap.Field{zap.String("event_domain", "article_generation")}
appendStringField := func(key, value string) {
if value = strings.TrimSpace(value); value != "" {
fields = append(fields, zap.String(key, value))
}
}
appendInt64Field := func(key string, value int64) {
if value > 0 {
fields = append(fields, zap.Int64(key, value))
}
}
appendInt64Field("task_id", ctx.TaskID)
appendInt64Field("tenant_id", ctx.TenantID)
appendInt64Field("operator_id", ctx.OperatorID)
appendInt64Field("article_id", ctx.ArticleID)
appendInt64Field("quota_reservation_id", ctx.ReservationID)
appendStringField("task_type", ctx.TaskType)
appendStringField("stage", ctx.Stage)
if ctx.AttemptCount > 0 {
fields = append(fields, zap.Int("attempt_count", ctx.AttemptCount))
}
appendStringField("lease_owner", ctx.LeaseOwner)
appendStringField("status_before", ctx.StatusBefore)
appendStringField("status_after", ctx.StatusAfter)
appendStringField("result", ctx.Result)
appendStringField("anomaly_type", ctx.AnomalyType)
appendStringField("severity", ctx.Severity)
if ctx.Duration > 0 {
fields = append(fields, zap.Duration("duration", ctx.Duration))
fields = append(fields, zap.Int64("duration_ms", ctx.Duration.Milliseconds()))
}
return fields
}
func LogGenerationTaskInfo(logger *zap.Logger, message string, ctx GenerationTaskLogContext, fields ...zap.Field) {
if logger == nil {
return
}
logger.Info(message, append(GenerationTaskLogFields(ctx), fields...)...)
}
func LogGenerationTaskWarn(logger *zap.Logger, message string, ctx GenerationTaskLogContext, fields ...zap.Field) {
if logger == nil {
return
}
logger.Warn(message, append(GenerationTaskLogFields(ctx), fields...)...)
}
func LogGenerationTaskError(logger *zap.Logger, message string, ctx GenerationTaskLogContext, fields ...zap.Field) {
if logger == nil {
return
}
logger.Error(message, append(GenerationTaskLogFields(ctx), fields...)...)
}
type GenerationTaskMetricsSnapshot struct {
TransitionTotal int64 `json:"transition_total"`
FailureTotal int64 `json:"failure_total"`
CleanupFailureTotal int64 `json:"cleanup_failure_total"`
QueueFailureTotal int64 `json:"queue_failure_total"`
RecoveryRequeueTotal int64 `json:"recovery_requeue_total"`
RecoveryFinalizeTotal int64 `json:"recovery_finalize_total"`
StateAnomalyTotal int64 `json:"state_anomaly_total"`
StateCheckFailureTotal int64 `json:"state_check_failure_total"`
LastEventAt *time.Time `json:"last_event_at,omitempty"`
LastEvent string `json:"last_event,omitempty"`
LastTaskType string `json:"last_task_type,omitempty"`
LastStage string `json:"last_stage,omitempty"`
LastResult string `json:"last_result,omitempty"`
LastAnomalyType string `json:"last_anomaly_type,omitempty"`
LastSeverity string `json:"last_severity,omitempty"`
}
type generationTaskMetricsRecorder struct {
transitionTotal atomic.Int64
failureTotal atomic.Int64
cleanupFailureTotal atomic.Int64
queueFailureTotal atomic.Int64
recoveryRequeueTotal atomic.Int64
recoveryFinalizeTotal atomic.Int64
stateAnomalyTotal atomic.Int64
stateCheckFailureTotal atomic.Int64
lastEventAtUnixNano atomic.Int64
lastEvent atomic.Value
lastTaskType atomic.Value
lastStage atomic.Value
lastResult atomic.Value
lastAnomalyType atomic.Value
lastSeverity atomic.Value
}
var generationTaskMetrics = newGenerationTaskMetricsRecorder()
func newGenerationTaskMetricsRecorder() *generationTaskMetricsRecorder {
return &generationTaskMetricsRecorder{}
}
func RecordGenerationTaskEvent(event string, ctx GenerationTaskLogContext) {
generationTaskMetrics.record(event, ctx)
}
func GenerationTaskMetricsSnapshotValue() GenerationTaskMetricsSnapshot {
return generationTaskMetrics.snapshot()
}
func resetGenerationTaskMetricsForTest() {
generationTaskMetrics = newGenerationTaskMetricsRecorder()
}
func (r *generationTaskMetricsRecorder) record(event string, ctx GenerationTaskLogContext) {
if r == nil {
return
}
event = strings.TrimSpace(event)
if event == "" {
event = "unknown"
}
switch event {
case GenerationTaskEventTransition:
r.transitionTotal.Add(1)
case GenerationTaskEventFailure:
r.failureTotal.Add(1)
case GenerationTaskEventCleanupFailure:
r.cleanupFailureTotal.Add(1)
case GenerationTaskEventQueueFailure:
r.queueFailureTotal.Add(1)
case GenerationTaskEventRecoveryRequeue:
r.recoveryRequeueTotal.Add(1)
case GenerationTaskEventRecoveryFinalize:
r.recoveryFinalizeTotal.Add(1)
case GenerationTaskEventStateAnomaly:
r.stateAnomalyTotal.Add(1)
case GenerationTaskEventStateCheckFailure:
r.stateCheckFailureTotal.Add(1)
}
r.lastEventAtUnixNano.Store(time.Now().UTC().UnixNano())
r.lastEvent.Store(event)
storeGenerationMetricString(&r.lastTaskType, ctx.TaskType)
storeGenerationMetricString(&r.lastStage, ctx.Stage)
storeGenerationMetricString(&r.lastResult, ctx.Result)
storeGenerationMetricString(&r.lastAnomalyType, ctx.AnomalyType)
storeGenerationMetricString(&r.lastSeverity, ctx.Severity)
}
func (r *generationTaskMetricsRecorder) snapshot() GenerationTaskMetricsSnapshot {
if r == nil {
return GenerationTaskMetricsSnapshot{}
}
return GenerationTaskMetricsSnapshot{
TransitionTotal: r.transitionTotal.Load(),
FailureTotal: r.failureTotal.Load(),
CleanupFailureTotal: r.cleanupFailureTotal.Load(),
QueueFailureTotal: r.queueFailureTotal.Load(),
RecoveryRequeueTotal: r.recoveryRequeueTotal.Load(),
RecoveryFinalizeTotal: r.recoveryFinalizeTotal.Load(),
StateAnomalyTotal: r.stateAnomalyTotal.Load(),
StateCheckFailureTotal: r.stateCheckFailureTotal.Load(),
LastEventAt: generationMetricUnixNanoTime(r.lastEventAtUnixNano.Load()),
LastEvent: loadGenerationMetricString(&r.lastEvent),
LastTaskType: loadGenerationMetricString(&r.lastTaskType),
LastStage: loadGenerationMetricString(&r.lastStage),
LastResult: loadGenerationMetricString(&r.lastResult),
LastAnomalyType: loadGenerationMetricString(&r.lastAnomalyType),
LastSeverity: loadGenerationMetricString(&r.lastSeverity),
}
}
func storeGenerationMetricString(target *atomic.Value, value string) {
if target == nil {
return
}
target.Store(strings.TrimSpace(value))
}
func loadGenerationMetricString(value *atomic.Value) string {
if value == nil {
return ""
}
raw := value.Load()
text, _ := raw.(string)
return text
}
func generationMetricUnixNanoTime(value int64) *time.Time {
if value <= 0 {
return nil
}
t := time.Unix(0, value).UTC()
return &t
}