prompt_logging.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package securityaudit
  2. import (
  3. "context"
  4. "log/slog"
  5. "strings"
  6. )
  7. const (
  8. EventConfigUpdated = "prompt_audit.config_updated"
  9. EventConfigLoaded = "prompt_guard.config_loaded"
  10. EventConfigReloadDegraded = "prompt_guard.config_reload_degraded"
  11. EventConfigTokenInvalid = "prompt_guard.config_token_invalid"
  12. EventProbeStarted = "prompt_audit.endpoint_probe_started"
  13. EventProbeFinished = "prompt_audit.endpoint_probe_finished"
  14. EventProbeFailed = "prompt_audit.endpoint_probe_failed"
  15. EventJobEnqueued = "prompt_audit.job_enqueued"
  16. EventEnqueueSkipped = "prompt_audit.enqueue_skipped"
  17. EventEnqueueDropped = "prompt_audit.enqueue_dropped"
  18. EventAuditStarted = "prompt_audit.started"
  19. EventProcessingReclaimed = "prompt_audit.processing_reclaimed"
  20. EventProcessed = "prompt_audit.processed"
  21. EventProcessFailed = "prompt_audit.process_failed"
  22. EventFindingRecorded = "prompt_audit.finding_recorded"
  23. EventChunkStarted = "prompt_audit.scan_chunk_started"
  24. EventChunkCompleted = "prompt_audit.scan_chunk_completed"
  25. EventChunkFailed = "prompt_audit.scan_chunk_failed"
  26. EventChunksAggregated = "prompt_audit.scan_chunks_aggregated"
  27. EventEvaluationStarted = "prompt_guard.evaluation_started"
  28. EventGuardAllowed = "prompt_guard.allowed"
  29. EventGuardBlocked = "prompt_guard.blocked"
  30. EventGuardFailed = "prompt_guard.failed"
  31. EventResultRecordFailed = "prompt_guard.result_record_failed"
  32. EventEventDeleted = "prompt_audit.event_deleted"
  33. EventEventsDeleted = "prompt_audit.events_deleted"
  34. EventDeletePreviewed = "prompt_audit.events_delete_previewed"
  35. EventEventsFilterDeleted = "prompt_audit.events_filter_deleted"
  36. )
  37. var knownLogEvents = map[string]struct{}{
  38. EventConfigUpdated: {}, EventConfigLoaded: {}, EventConfigReloadDegraded: {}, EventConfigTokenInvalid: {},
  39. EventProbeStarted: {}, EventProbeFinished: {}, EventProbeFailed: {},
  40. EventJobEnqueued: {}, EventEnqueueSkipped: {}, EventEnqueueDropped: {},
  41. EventAuditStarted: {}, EventProcessingReclaimed: {}, EventProcessed: {}, EventProcessFailed: {}, EventFindingRecorded: {},
  42. EventChunkStarted: {}, EventChunkCompleted: {}, EventChunkFailed: {}, EventChunksAggregated: {},
  43. EventEvaluationStarted: {}, EventGuardAllowed: {}, EventGuardBlocked: {}, EventGuardFailed: {}, EventResultRecordFailed: {},
  44. EventEventDeleted: {}, EventEventsDeleted: {}, EventDeletePreviewed: {}, EventEventsFilterDeleted: {},
  45. }
  46. var allowedLogFields = map[string]struct{}{
  47. "request_id": {}, "user_id": {}, "api_key_id": {}, "group_id": {}, "provider": {},
  48. "protocol": {}, "endpoint": {}, "model": {}, "job_id": {}, "event_id": {},
  49. "config_version": {}, "guard_endpoint_id": {}, "decision": {}, "risk_level": {},
  50. "action": {}, "chunk_index": {}, "chunk_total": {}, "chunk_chars": {}, "input_chars": {},
  51. "input_limit": {}, "latency_ms": {}, "status": {}, "error_code": {}, "error_kind": {},
  52. "queue_length": {}, "queue_capacity": {}, "stage": {}, "upstream_dispatched": {},
  53. "billing_preconsumed": {}, "worker_id": {}, "reclaimed_total": {}, "attempts": {},
  54. "max_attempts": {}, "claim_version": {}, "http_status": {}, "retryable": {},
  55. }
  56. func LogInfo(event string, fields map[string]any) {
  57. if _, ok := knownLogEvents[event]; !ok {
  58. return
  59. }
  60. slog.LogAttrs(context.Background(), slog.LevelInfo, event, safeAttrs(fields)...)
  61. }
  62. func LogWarn(event string, fields map[string]any) {
  63. if _, ok := knownLogEvents[event]; !ok {
  64. return
  65. }
  66. slog.LogAttrs(context.Background(), slog.LevelWarn, event, safeAttrs(fields)...)
  67. }
  68. func LogError(event string, fields map[string]any) {
  69. if _, ok := knownLogEvents[event]; !ok {
  70. return
  71. }
  72. slog.LogAttrs(context.Background(), slog.LevelError, event, safeAttrs(fields)...)
  73. }
  74. func safeAttrs(fields map[string]any) []slog.Attr {
  75. attrs := make([]slog.Attr, 0, len(fields))
  76. for key, value := range fields {
  77. key = strings.TrimSpace(key)
  78. if _, allowed := allowedLogFields[key]; !allowed {
  79. continue
  80. }
  81. if text, ok := value.(string); ok {
  82. if key == "error_kind" || key == "error_code" {
  83. value = stableErrorCode(text)
  84. } else {
  85. value = TrimRunes(strings.TrimSpace(text), 256)
  86. }
  87. }
  88. attrs = append(attrs, slog.Any(key, value))
  89. }
  90. return attrs
  91. }
  92. func mergeLogFields(base map[string]any, extra map[string]any) map[string]any {
  93. result := make(map[string]any, len(base)+len(extra))
  94. for key, value := range base {
  95. result[key] = value
  96. }
  97. for key, value := range extra {
  98. result[key] = value
  99. }
  100. return result
  101. }
  102. func requestLogFields(req Request) map[string]any {
  103. return map[string]any{
  104. "request_id": req.RequestID, "user_id": req.UserID, "api_key_id": req.APIKeyID,
  105. "group_id": pointerLogID(req.GroupID), "provider": req.Provider, "protocol": req.Protocol,
  106. "endpoint": req.Endpoint, "model": req.Model, "stage": req.Stage,
  107. }
  108. }
  109. func snapshotLogFields(snapshot PromptSnapshot) map[string]any {
  110. return map[string]any{
  111. "request_id": snapshot.RequestID, "user_id": snapshot.UserID, "api_key_id": snapshot.APIKeyID,
  112. "group_id": pointerLogID(snapshot.GroupID), "provider": snapshot.Provider, "protocol": snapshot.Protocol,
  113. "endpoint": snapshot.Endpoint, "model": snapshot.Model, "stage": snapshot.Stage,
  114. }
  115. }
  116. func jobLogFields(job *Job) map[string]any {
  117. if job == nil {
  118. return map[string]any{}
  119. }
  120. fields := snapshotLogFields(job.Snapshot)
  121. fields["job_id"] = job.ID
  122. fields["config_version"] = job.ConfigVersion
  123. fields["claim_version"] = job.ClaimVersion
  124. return fields
  125. }
  126. func stableErrorCode(code string) string {
  127. code = strings.ToLower(strings.TrimSpace(code))
  128. if code == "" {
  129. return "unknown_error"
  130. }
  131. for _, char := range code {
  132. if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' {
  133. continue
  134. }
  135. return "redacted_error"
  136. }
  137. return TrimRunes(code, 64)
  138. }
  139. func stableErrorMessage(code string) string {
  140. switch stableErrorCode(code) {
  141. case ErrorCodeBlocked:
  142. return "Prompt Guard blocked the request"
  143. case ErrorCodeUnavailable, "payload_store_unavailable", "payload_missing":
  144. return "Prompt Audit dependency is unavailable"
  145. case ErrorCodeInvalidResponse:
  146. return "Prompt Guard returned an invalid response"
  147. case "queue_full", "queue_admission_busy":
  148. return "Prompt Audit queue is unavailable"
  149. case "worker_panic":
  150. return "Prompt Audit worker failed"
  151. case "config_load_failed", "config_ttl_reload_failed", "config_invalidation_reload_failed":
  152. return "Prompt Audit configuration could not be loaded"
  153. default:
  154. return "Prompt Audit operation failed"
  155. }
  156. }
  157. func sanitizeStoredError(code string) (string, string) {
  158. stableCode := stableErrorCode(code)
  159. return stableCode, TrimRunes(stableErrorMessage(stableCode), 160)
  160. }