package persistence import ( "context" "crypto/rand" "encoding/json" "fmt" "log/slog" "time" "github.com/jackc/pgx/v5/pgxpool" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" ) var ( persistTracer = otel.Tracer("otel-bi.persistence") persistMeter = otel.Meter("otel-bi.persistence") persistWritesTotal, _ = persistMeter.Int64Counter( "persistence.writes_total", metric.WithDescription("Total persistence write operations"), ) ) // newUUID generates a random UUID v4. func newUUID() string { var b [16]byte rand.Read(b[:]) //nolint:errcheck b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } // spanContext extracts trace_id and span_id from the current span as nullable strings. func spanContext(span trace.Span) (traceID, spanID *string) { sctx := span.SpanContext() if !sctx.IsValid() { return nil, nil } tid := sctx.TraceID().String() sid := sctx.SpanID().String() return &tid, &sid } // mustJSON marshals v to JSON bytes, returning nil on error. func mustJSON(v any) []byte { b, _ := json.Marshal(v) return b } // --------------------------------------------------------------------------- // Job execution tracking // --------------------------------------------------------------------------- func RecordJobStart(ctx context.Context, pool *pgxpool.Pool, jobName, domain string, traceID, spanID *string) string { ctx, span := persistTracer.Start(ctx, "persistence.record_job_start") defer span.End() id := newUUID() _, err := pool.Exec(ctx, `INSERT INTO job_executions (id, started_at, job_name, domain, status, trace_id, span_id) VALUES ($1, NOW(), $2, $3, 'running', $4, $5)`, id, jobName, domain, traceID, spanID, ) if err != nil { slog.Warn("failed to record job start", "job", jobName, "err", err) } return id } func RecordJobComplete(ctx context.Context, pool *pgxpool.Pool, jobID string, startedAt time.Time, records int) { ctx, span := persistTracer.Start(ctx, "persistence.record_job_complete") defer span.End() durationMs := int64(time.Since(startedAt).Milliseconds()) _, err := pool.Exec(ctx, `UPDATE job_executions SET status = 'success', completed_at = NOW(), duration_ms = $2, records_processed = $3 WHERE id = $1`, jobID, durationMs, records, ) if err != nil { slog.Warn("failed to record job complete", "id", jobID, "err", err) } } func RecordJobFailure(ctx context.Context, pool *pgxpool.Pool, jobID string, startedAt time.Time, errMsg string) { ctx, span := persistTracer.Start(ctx, "persistence.record_job_failure") defer span.End() durationMs := int64(time.Since(startedAt).Milliseconds()) if len(errMsg) > 2000 { errMsg = errMsg[:2000] } _, err := pool.Exec(ctx, `UPDATE job_executions SET status = 'failure', completed_at = NOW(), duration_ms = $2, error_message = $3 WHERE id = $1`, jobID, durationMs, errMsg, ) if err != nil { slog.Warn("failed to record job failure", "id", jobID, "err", err) } } // --------------------------------------------------------------------------- // Audit log // --------------------------------------------------------------------------- type AuditEntry struct { Action string ActorType string ActorID string Domain string Service string EntityType string Status string Payload any } func AppendAudit(ctx context.Context, pool *pgxpool.Pool, e AuditEntry) { ctx, span := persistTracer.Start(ctx, "persistence.append_audit", trace.WithAttributes( attribute.String("audit.action", e.Action), attribute.String("audit.domain", e.Domain), ), ) defer span.End() traceID, spanID := spanContext(span) status := e.Status if status == "" { status = "success" } payloadJSON := mustJSON(e.Payload) _, err := pool.Exec(ctx, `INSERT INTO audit_log (id, occurred_at, action, status, actor_type, actor_id, domain, service, entity_type, trace_id, span_id, payload) VALUES ($1, NOW(), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb)`, newUUID(), e.Action, status, e.ActorType, e.ActorID, e.Domain, e.Service, e.EntityType, traceID, spanID, payloadJSON, ) if err != nil { slog.Warn("failed to append audit", "action", e.Action, "err", err) } }