Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions core/applog/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,16 @@ func L(ctx context.Context) *slog.Logger {

// Fatal logs at error level and exits the process.
func Fatal(ctx context.Context, msg string, keysAndValues ...interface{}) {
attrs := keysAndValues
ev := Event{
Message: msg,
Code: errcode.InternalError,
Component: "server",
Status: "failure",
}
if len(attrs) > 0 {
ev.Context = kvPairsToMap(attrs)
if len(keysAndValues) > 0 {
ev.Context = kvPairsToMap(keysAndValues)
}
ErrorCode(ctx, errcode.InternalError, msg, ev)
Error(ctx, ev)
os.Exit(1)
}

Expand Down
8 changes: 1 addition & 7 deletions core/applog/scrub.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package applog

import "strings"

// RedactedPlaceholder replaces secret values in logs and dumps.
const RedactedPlaceholder = "***"

// ScrubMap returns a copy of fields with secret field values redacted.
func ScrubMap(fields map[string]interface{}) map[string]interface{} {
if fields == nil {
return nil
Expand All @@ -17,7 +15,6 @@ func ScrubMap(fields map[string]interface{}) map[string]interface{} {
return scrubbed
}

// ScrubValue redacts value when fieldName is secret; walks nested maps and slices.
func ScrubValue(fieldName string, value any) any {
if IsSecretKey(fieldName) {
return RedactedPlaceholder
Expand Down Expand Up @@ -48,14 +45,12 @@ func ScrubValue(fieldName string, value any) any {
}
}

// IsSecretKey reports whether a field name must never be logged in cleartext.
// "sid" matches the full field name only (avoids false positives like "inside").
// IsSecretKey: "sid" is exact-match only (avoids "inside").
func IsSecretKey(fieldName string) bool {
normalized := strings.ToLower(strings.TrimSpace(fieldName))
return normalized == "sid" || (normalized != "" && containsSecretKeyword(normalized))
}

// TextContainsSecretKeyword reports whether free text (e.g. SQL) mentions a secret term.
func TextContainsSecretKeyword(text string) bool {
lowered := strings.ToLower(text)
return strings.Contains(lowered, "sid") || containsSecretKeyword(lowered)
Expand All @@ -70,7 +65,6 @@ func containsSecretKeyword(haystack string) bool {
return false
}

// secretKeywords match as substrings of field names and of free text (including totp*).
var secretKeywords = []string{
"password", "token", "secret", "authorization", "cookie",
"session", "api_key", "apikey", "key_hash", "csrf", "totp",
Expand Down
27 changes: 6 additions & 21 deletions core/applog/stdio.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,27 @@ package applog

import "context"

// InfoMsg logs a structured info event with the logging contract.
func InfoMsg(ctx context.Context, component, operation, message string, ctxFields map[string]interface{}) {
Info(ctx, Event{
Message: message,
Component: component,
Operation: operation,
Status: "success",
Context: ctxFields,
Message: message, Component: component, Operation: operation,
Status: "success", Context: ctxFields,
})
}

// WarnMsg logs a structured warning event with the logging contract.
func WarnMsg(ctx context.Context, component, operation, message string, err error, ctxFields map[string]interface{}) {
Warn(ctx, Event{
Message: message,
Component: component,
Operation: operation,
Status: "partial",
Context: ctxFields,
Err: err,
Message: message, Component: component, Operation: operation,
Status: "partial", Context: ctxFields, Err: err,
})
}

// DebugMsg logs a structured debug event with the logging contract.
func DebugMsg(ctx context.Context, component, operation, message string, ctxFields map[string]interface{}) {
Debug(ctx, Event{
Message: message,
Component: component,
Operation: operation,
Status: "success",
Context: ctxFields,
Message: message, Component: component, Operation: operation,
Status: "success", Context: ctxFields,
})
}

// ErrorCode logs a failure with a stable machine code and human message.
func ErrorCode(ctx context.Context, code, message string, ev Event) {
ev.Code = code
if message != "" {
Expand All @@ -48,7 +34,6 @@ func ErrorCode(ctx context.Context, code, message string, ev Event) {
Error(ctx, ev)
}

// WarnCode logs a warning with a stable machine code and human message.
func WarnCode(ctx context.Context, code, message string, ev Event) {
ev.Code = code
if message != "" {
Expand Down
39 changes: 17 additions & 22 deletions core/orm/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,10 @@ func auditValues(ctx context.Context, action, model string, resID int64, before,
uid := SecurityUID(ctx)
var beforeJSON, afterJSON string
if before != nil {
if b, err := json.Marshal(scrubAuditMap(before)); err == nil {
beforeJSON = string(b)
} else {
applog.WarnCode(ctx, errcode.InternalError, "Audit before_json marshal failed", applog.Event{
Component: "orm",
Operation: "audit",
Status: "partial",
Context: map[string]interface{}{"resource": model, "resource_id": resID},
Err: err,
})
}
beforeJSON = marshalAuditJSON(ctx, "before_json", model, resID, before)
}
if after != nil {
if b, err := json.Marshal(scrubAuditMap(after)); err == nil {
afterJSON = string(b)
} else {
applog.WarnCode(ctx, errcode.InternalError, "Audit after_json marshal failed", applog.Event{
Component: "orm",
Operation: "audit",
Status: "partial",
Context: map[string]interface{}{"resource": model, "resource_id": resID},
Err: err,
})
}
afterJSON = marshalAuditJSON(ctx, "after_json", model, resID, after)
}
vals := map[string]interface{}{
"action": action,
Expand All @@ -63,6 +43,21 @@ func auditValues(ctx context.Context, action, model string, resID int64, before,
return vals
}

func marshalAuditJSON(ctx context.Context, field, model string, resID int64, values map[string]interface{}) string {
b, err := json.Marshal(scrubAuditMap(values))
if err != nil {
applog.WarnCode(ctx, errcode.InternalError, "Audit "+field+" marshal failed", applog.Event{
Component: "orm",
Operation: "audit",
Status: "partial",
Context: map[string]interface{}{"resource": model, "resource_id": resID},
Err: err,
})
return ""
}
return string(b)
}

// AppendAudit writes an immutable audit row (best-effort; never fails the caller).
func AppendAudit(ctx context.Context, action, model string, resID int64, before, after map[string]interface{}, detail string) {
AppendAuditTx(ctx, nil, action, model, resID, before, after, detail)
Expand Down
31 changes: 31 additions & 0 deletions core/orm/classify_log_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package orm

import (
"errors"
"strings"

"sumeru/core/errcode"
)

// ClassifyLogCode maps an error to a stable applog/errcode machine id.
func ClassifyLogCode(err error) string {
if err == nil {
return errcode.InternalError
}
if IsAccessDenied(err) || IsRecordRuleFailed(err) {
return errcode.AccessDenied
}
var fieldErr *FieldValidationError
if errors.As(err, &fieldErr) {
return errcode.ValidationError
}
msg := strings.ToLower(err.Error())
switch {
case strings.Contains(msg, "record(s) not found"), strings.Contains(msg, "not found"):
return errcode.RecordNotFound
case strings.Contains(msg, "validation"), strings.Contains(msg, "invalid"), strings.Contains(msg, "required"):
return errcode.ValidationError
default:
return errcode.InternalError
}
}
6 changes: 3 additions & 3 deletions core/orm/crud_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func Search(ctx context.Context, modelName string, domain [][]interface{}) (resu
if results != nil {
n = len(results)
}
logORMOperationKV(ctx, start, "search", modelName, err, "rows", n)
logORMOperation(ctx, start, "search", modelName, err, map[string]interface{}{"rows": n})
}()
results, err = execSearchQuery(ctx, modelName, domain, nil)
if err != nil {
Expand Down Expand Up @@ -124,7 +124,7 @@ func SearchPage(ctx context.Context, modelName string, domain [][]interface{}, l
if results != nil {
n = len(results)
}
logORMOperationKV(ctx, start, "search_page", modelName, err, "rows", n, "limit", limit, "offset", offset)
logORMOperation(ctx, start, "search_page", modelName, err, map[string]interface{}{"rows": n, "limit": limit, "offset": offset})
}()
if limit <= 0 || limit > maxSearchLimit {
limit = maxSearchLimit
Expand All @@ -150,7 +150,7 @@ func SearchPage(ctx context.Context, modelName string, domain [][]interface{}, l
func SearchCount(ctx context.Context, modelName string, domain [][]interface{}) (n int, err error) {
start := time.Now()
defer func() {
logORMOperationKV(ctx, start, "search_count", modelName, err, "count", n)
logORMOperation(ctx, start, "search_count", modelName, err, map[string]interface{}{"count": n})
}()
ctx = ContextWithReadReplica(ctx, true)
_, whereClause, args, _, err := prepareSearchRead(ctx, modelName, domain)
Expand Down
2 changes: 1 addition & 1 deletion core/orm/crud_search_one.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func CriteriaToDomain(criteria map[string]interface{}) [][]interface{} {
func SearchOne(ctx context.Context, modelName string, criteria map[string]interface{}) (result map[string]interface{}, err error) {
start := time.Now()
defer func() {
logORMOperationKV(ctx, start, "search_one", modelName, err, "has_row", result != nil)
logORMOperation(ctx, start, "search_one", modelName, err, map[string]interface{}{"has_row": result != nil})
}()
if _, ok := Registry[modelName]; !ok {
return nil, fmt.Errorf("model %s not registered", modelName)
Expand Down
10 changes: 1 addition & 9 deletions core/orm/crud_sideeffects.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,7 @@ func emitSideEffectsOnTx(ctx context.Context, tx TxWrapper, modelName string, ui
"model": modelName,
"id": int(row.ResID),
}); err != nil {
applog.WarnCode(ctx, errcode.InternalError, "outbox enqueue after mutation failed", applog.Event{
Component: "orm",
Operation: "side_effects",
Status: "partial",
Context: map[string]interface{}{
"model": modelName, "event": row.EventName,
},
Err: err,
})
logSideEffectWarn(ctx, "outbox_enqueue", modelName, err, "event", row.EventName)
}
}
}
Expand Down
11 changes: 1 addition & 10 deletions core/orm/crud_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import (
"context"
"fmt"
"strings"

"sumeru/core/applog"
"sumeru/core/errcode"
)

func insertPreparedOnTx(ctx context.Context, tx TxWrapper, model Model, prepared map[string]interface{}) (int, error) {
Expand Down Expand Up @@ -106,13 +103,7 @@ func insertSideEffectRow(ctx context.Context, tx TxWrapper, registryKey string,
}
_, err := Create(bypass, inst, vals)
if err != nil {
applog.WarnCode(ctx, errcode.InternalError, "Side effect insert failed", applog.Event{
Component: "orm",
Operation: "insert_side_effect",
Status: "partial",
Context: map[string]interface{}{"resource": registryKey},
Err: err,
})
logSideEffectWarn(ctx, "insert_side_effect", registryKey, err)
}
return err
}
Loading
Loading