claude-code-proxy/proxy/internal/service/storage_sqlite.go

879 lines
23 KiB
Go

package service
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/seifghazi/claude-code-monitor/internal/config"
"github.com/seifghazi/claude-code-monitor/internal/model"
)
type sqliteStorageService struct {
db *sql.DB
config *config.StorageConfig
logger *log.Logger
// Prepared statements for frequently used queries
stmtInsertRequest *sql.Stmt
stmtUpdateResponse *sql.Stmt
stmtUpdateGrading *sql.Stmt
stmtGetRequestByID *sql.Stmt
stmtGetRequestsPage *sql.Stmt
stmtGetRequestsCount *sql.Stmt
stmtDeleteOldRequests *sql.Stmt
}
func NewSQLiteStorageService(cfg *config.StorageConfig) (StorageService, error) {
return NewSQLiteStorageServiceWithLogger(cfg, log.Default())
}
func NewSQLiteStorageServiceWithLogger(cfg *config.StorageConfig, logger *log.Logger) (StorageService, error) {
// Enable WAL mode and other optimizations via connection string
// _journal_mode=WAL: Write-Ahead Logging for better concurrent read performance
// _synchronous=NORMAL: Good balance of safety and performance
// _busy_timeout=5000: Wait up to 5 seconds if database is locked
// _cache_size=-20000: Use 20MB of memory for cache (negative = KB)
connStr := cfg.DBPath + "?_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=5000&_cache_size=-20000"
db, err := sql.Open("sqlite3", connStr)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Configure connection pool
// SQLite only supports one writer at a time, but can handle multiple readers
db.SetMaxOpenConns(1) // Serialize writes to avoid SQLITE_BUSY errors
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(time.Hour)
// Verify connection
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
service := &sqliteStorageService{
db: db,
config: cfg,
logger: logger,
}
if err := service.createTables(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to create tables: %w", err)
}
if err := service.prepareStatements(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to prepare statements: %w", err)
}
if err := service.cleanupExpiredRequests(); err != nil {
logger.Printf("Warning: failed to apply retention policy during startup: %v", err)
}
return service, nil
}
func (s *sqliteStorageService) createTables() error {
schema := `
CREATE TABLE IF NOT EXISTS requests (
id TEXT PRIMARY KEY,
timestamp DATETIME NOT NULL,
method TEXT NOT NULL,
endpoint TEXT NOT NULL,
headers TEXT NOT NULL,
body TEXT NOT NULL,
user_agent TEXT,
content_type TEXT,
prompt_grade TEXT,
response TEXT,
model TEXT,
original_model TEXT,
routed_model TEXT
);
-- Index for listing requests by time (most common query)
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp DESC);
-- Index for filtering by model
CREATE INDEX IF NOT EXISTS idx_requests_model ON requests(model);
-- Index for filtering by endpoint
CREATE INDEX IF NOT EXISTS idx_requests_endpoint ON requests(endpoint);
`
_, err := s.db.Exec(schema)
if err != nil {
return err
}
// Run migrations
s.migrateSchema()
return nil
}
func (s *sqliteStorageService) migrateSchema() {
// Ensure WAL mode is enabled (in case opened without connection string params)
_, err := s.db.Exec("PRAGMA journal_mode=WAL")
if err != nil {
s.logger.Printf("Warning: failed to set WAL mode: %v", err)
}
// Drop old redundant index if it exists (we renamed to idx_requests_timestamp)
s.db.Exec("DROP INDEX IF EXISTS idx_timestamp")
}
func (s *sqliteStorageService) prepareStatements() error {
var err error
s.stmtInsertRequest, err = s.db.Prepare(`
INSERT INTO requests (id, timestamp, method, endpoint, headers, body, user_agent, content_type, model, original_model, routed_model)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare insert statement: %w", err)
}
s.stmtUpdateResponse, err = s.db.Prepare(`
UPDATE requests SET response = ? WHERE id = ?
`)
if err != nil {
return fmt.Errorf("failed to prepare update response statement: %w", err)
}
s.stmtUpdateGrading, err = s.db.Prepare(`
UPDATE requests SET prompt_grade = ? WHERE id = ?
`)
if err != nil {
return fmt.Errorf("failed to prepare update grading statement: %w", err)
}
s.stmtGetRequestByID, err = s.db.Prepare(`
SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model
FROM requests
WHERE id = ?
`)
if err != nil {
return fmt.Errorf("failed to prepare get by ID statement: %w", err)
}
s.stmtGetRequestsPage, err = s.db.Prepare(`
SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model
FROM requests
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
`)
if err != nil {
return fmt.Errorf("failed to prepare get requests page statement: %w", err)
}
s.stmtGetRequestsCount, err = s.db.Prepare(`
SELECT COUNT(*) FROM requests
`)
if err != nil {
return fmt.Errorf("failed to prepare count statement: %w", err)
}
s.stmtDeleteOldRequests, err = s.db.Prepare(`
DELETE FROM requests WHERE timestamp < ?
`)
if err != nil {
return fmt.Errorf("failed to prepare delete old requests statement: %w", err)
}
return nil
}
func (s *sqliteStorageService) SaveRequest(request *model.RequestLog) (string, error) {
headersJSON, err := json.Marshal(request.Headers)
if err != nil {
return "", fmt.Errorf("failed to marshal headers: %w", err)
}
bodyForStorage, err := s.prepareRequestBodyForStorage(request.Body)
if err != nil {
return "", fmt.Errorf("failed to prepare body for storage: %w", err)
}
bodyJSON, err := json.Marshal(bodyForStorage)
if err != nil {
return "", fmt.Errorf("failed to marshal body: %w", err)
}
_, err = s.stmtInsertRequest.Exec(
request.RequestID,
request.Timestamp,
request.Method,
request.Endpoint,
string(headersJSON),
string(bodyJSON),
request.UserAgent,
request.ContentType,
request.Model,
request.OriginalModel,
request.RoutedModel,
)
if err != nil {
return "", fmt.Errorf("failed to insert request: %w", err)
}
if err := s.cleanupExpiredRequests(); err != nil {
s.logger.Printf("Warning: failed to apply retention policy: %v", err)
}
return request.RequestID, nil
}
func (s *sqliteStorageService) GetRequests(page, limit int, modelFilter string) ([]model.RequestLog, int, error) {
whereClause := ""
countArgs := []interface{}{}
queryArgs := []interface{}{}
if modelFilter != "" && modelFilter != "all" {
// Escape LIKE special characters to prevent pattern injection
escapedFilter := escapeLikePattern(strings.ToLower(modelFilter))
whereClause = " WHERE LOWER(model) LIKE ? ESCAPE '\\'"
filterValue := "%" + escapedFilter + "%"
countArgs = append(countArgs, filterValue)
queryArgs = append(queryArgs, filterValue)
}
// Get total count
var total int
countQuery := "SELECT COUNT(*) FROM requests" + whereClause
err := s.db.QueryRow(countQuery, countArgs...).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("failed to get total count: %w", err)
}
// Get paginated results
offset := (page - 1) * limit
query := `
SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model
FROM requests` + whereClause + `
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
`
queryArgs = append(queryArgs, limit, offset)
rows, err := s.db.Query(query, queryArgs...)
if err != nil {
return nil, 0, fmt.Errorf("failed to query requests: %w", err)
}
defer rows.Close()
requests, err := s.scanRequestRows(rows)
if err != nil {
return nil, 0, err
}
return requests, total, nil
}
func (s *sqliteStorageService) ClearRequests() (int, error) {
result, err := s.db.Exec("DELETE FROM requests")
if err != nil {
return 0, fmt.Errorf("failed to clear requests: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %w", err)
}
// Reclaim space after clearing all data
_, err = s.db.Exec("VACUUM")
if err != nil {
s.logger.Printf("Warning: failed to vacuum database: %v", err)
}
return int(rowsAffected), nil
}
func (s *sqliteStorageService) UpdateRequestWithGrading(requestID string, grade *model.PromptGrade) error {
gradeJSON, err := json.Marshal(grade)
if err != nil {
return fmt.Errorf("failed to marshal grade: %w", err)
}
result, err := s.stmtUpdateGrading.Exec(string(gradeJSON), requestID)
if err != nil {
return fmt.Errorf("failed to update request with grading: %w", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("request %s not found", requestID)
}
if err := s.cleanupExpiredRequests(); err != nil {
s.logger.Printf("Warning: failed to apply retention policy: %v", err)
}
return nil
}
func (s *sqliteStorageService) UpdateRequestWithResponse(request *model.RequestLog) error {
responseForStorage, err := s.prepareResponseForStorage(request.Response)
if err != nil {
return fmt.Errorf("failed to prepare response for storage: %w", err)
}
responseJSON, err := json.Marshal(responseForStorage)
if err != nil {
return fmt.Errorf("failed to marshal response: %w", err)
}
result, err := s.stmtUpdateResponse.Exec(string(responseJSON), request.RequestID)
if err != nil {
return fmt.Errorf("failed to update request with response: %w", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("request %s not found", request.RequestID)
}
return nil
}
// SaveRequestWithResponse saves a request and its response in a single transaction
func (s *sqliteStorageService) SaveRequestWithResponse(request *model.RequestLog) error {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
headersJSON, err := json.Marshal(request.Headers)
if err != nil {
return fmt.Errorf("failed to marshal headers: %w", err)
}
bodyForStorage, err := s.prepareRequestBodyForStorage(request.Body)
if err != nil {
return fmt.Errorf("failed to prepare body for storage: %w", err)
}
bodyJSON, err := json.Marshal(bodyForStorage)
if err != nil {
return fmt.Errorf("failed to marshal body: %w", err)
}
// Insert request
_, err = tx.Stmt(s.stmtInsertRequest).Exec(
request.RequestID,
request.Timestamp,
request.Method,
request.Endpoint,
string(headersJSON),
string(bodyJSON),
request.UserAgent,
request.ContentType,
request.Model,
request.OriginalModel,
request.RoutedModel,
)
if err != nil {
return fmt.Errorf("failed to insert request: %w", err)
}
// Update with response if present
if request.Response != nil {
responseForStorage, err := s.prepareResponseForStorage(request.Response)
if err != nil {
return fmt.Errorf("failed to prepare response for storage: %w", err)
}
responseJSON, err := json.Marshal(responseForStorage)
if err != nil {
return fmt.Errorf("failed to marshal response: %w", err)
}
_, err = tx.Stmt(s.stmtUpdateResponse).Exec(string(responseJSON), request.RequestID)
if err != nil {
return fmt.Errorf("failed to update response: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
if err := s.cleanupExpiredRequests(); err != nil {
s.logger.Printf("Warning: failed to apply retention policy: %v", err)
}
return nil
}
func (s *sqliteStorageService) EnsureDirectoryExists() error {
// No directory needed for SQLite
return nil
}
func (s *sqliteStorageService) GetRequestByShortID(shortID string) (*model.RequestLog, string, error) {
// Escape LIKE special characters to prevent pattern injection
escapedID := escapeLikePattern(shortID)
query := `
SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model
FROM requests
WHERE id LIKE ? ESCAPE '\'
ORDER BY timestamp DESC
LIMIT 1
`
var req model.RequestLog
var headersJSON, bodyJSON string
var promptGradeJSON, responseJSON sql.NullString
err := s.db.QueryRow(query, "%"+escapedID).Scan(
&req.RequestID,
&req.Timestamp,
&req.Method,
&req.Endpoint,
&headersJSON,
&bodyJSON,
&req.Model,
&req.UserAgent,
&req.ContentType,
&promptGradeJSON,
&responseJSON,
&req.OriginalModel,
&req.RoutedModel,
)
if err == sql.ErrNoRows {
return nil, "", fmt.Errorf("request with ID %s not found", shortID)
}
if err != nil {
return nil, "", fmt.Errorf("failed to query request: %w", err)
}
if err := s.unmarshalRequestFields(&req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil {
return nil, "", err
}
return &req, req.RequestID, nil
}
func (s *sqliteStorageService) GetConfig() *config.StorageConfig {
return s.config
}
func (s *sqliteStorageService) GetAllRequests(modelFilter string) ([]*model.RequestLog, error) {
return s.GetAllRequestsWithLimit(modelFilter, 0) // 0 means no limit
}
// GetAllRequestsWithLimit returns requests with an optional limit (0 = no limit)
func (s *sqliteStorageService) GetAllRequestsWithLimit(modelFilter string, limit int) ([]*model.RequestLog, error) {
var query string
args := []interface{}{}
if modelFilter != "" && modelFilter != "all" {
// Escape LIKE special characters
escapedFilter := escapeLikePattern(strings.ToLower(modelFilter))
query = `
SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model
FROM requests
WHERE LOWER(model) LIKE ? ESCAPE '\'
ORDER BY timestamp DESC
`
args = append(args, "%"+escapedFilter+"%")
} else {
query = `
SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model
FROM requests
ORDER BY timestamp DESC
`
}
if limit > 0 {
query += " LIMIT ?"
args = append(args, limit)
}
rows, err := s.db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query requests: %w", err)
}
defer rows.Close()
var requests []*model.RequestLog
for rows.Next() {
req, err := s.scanSingleRow(rows)
if err != nil {
s.logger.Printf("Warning: failed to scan request row: %v", err)
continue
}
requests = append(requests, req)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating rows: %w", err)
}
return requests, nil
}
// DeleteRequestsOlderThan removes requests older than the specified duration
func (s *sqliteStorageService) DeleteRequestsOlderThan(age time.Duration) (int, error) {
cutoff := time.Now().Add(-age)
result, err := s.stmtDeleteOldRequests.Exec(cutoff.Format(time.RFC3339))
if err != nil {
return 0, fmt.Errorf("failed to delete old requests: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %w", err)
}
return int(rowsAffected), nil
}
// GetDatabaseStats returns statistics about the database
func (s *sqliteStorageService) GetDatabaseStats() (map[string]interface{}, error) {
stats := make(map[string]interface{})
// Get row count
var count int
err := s.stmtGetRequestsCount.QueryRow().Scan(&count)
if err != nil {
return nil, fmt.Errorf("failed to get count: %w", err)
}
stats["total_requests"] = count
// Get database size
var pageCount, pageSize int
err = s.db.QueryRow("PRAGMA page_count").Scan(&pageCount)
if err == nil {
err = s.db.QueryRow("PRAGMA page_size").Scan(&pageSize)
if err == nil {
stats["database_size_bytes"] = pageCount * pageSize
}
}
// Get oldest and newest timestamps
var oldest, newest sql.NullString
err = s.db.QueryRow("SELECT MIN(timestamp), MAX(timestamp) FROM requests").Scan(&oldest, &newest)
if err == nil {
if oldest.Valid {
stats["oldest_request"] = oldest.String
}
if newest.Valid {
stats["newest_request"] = newest.String
}
}
return stats, nil
}
func (s *sqliteStorageService) Close() error {
// Close prepared statements
if s.stmtInsertRequest != nil {
s.stmtInsertRequest.Close()
}
if s.stmtUpdateResponse != nil {
s.stmtUpdateResponse.Close()
}
if s.stmtUpdateGrading != nil {
s.stmtUpdateGrading.Close()
}
if s.stmtGetRequestByID != nil {
s.stmtGetRequestByID.Close()
}
if s.stmtGetRequestsPage != nil {
s.stmtGetRequestsPage.Close()
}
if s.stmtGetRequestsCount != nil {
s.stmtGetRequestsCount.Close()
}
if s.stmtDeleteOldRequests != nil {
s.stmtDeleteOldRequests.Close()
}
// Checkpoint WAL before closing
_, err := s.db.Exec("PRAGMA wal_checkpoint(TRUNCATE)")
if err != nil {
s.logger.Printf("Warning: failed to checkpoint WAL: %v", err)
}
return s.db.Close()
}
// Helper functions
const redactionPlaceholder = "[REDACTED]"
// escapeLikePattern escapes special characters in LIKE patterns
func escapeLikePattern(s string) string {
// Escape \, %, and _ characters
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `%`, `\%`)
s = strings.ReplaceAll(s, `_`, `\_`)
return s
}
// scanRequestRows scans multiple rows into a slice of RequestLog
func (s *sqliteStorageService) scanRequestRows(rows *sql.Rows) ([]model.RequestLog, error) {
var requests []model.RequestLog
for rows.Next() {
var req model.RequestLog
var headersJSON, bodyJSON string
var promptGradeJSON, responseJSON sql.NullString
err := rows.Scan(
&req.RequestID,
&req.Timestamp,
&req.Method,
&req.Endpoint,
&headersJSON,
&bodyJSON,
&req.Model,
&req.UserAgent,
&req.ContentType,
&promptGradeJSON,
&responseJSON,
&req.OriginalModel,
&req.RoutedModel,
)
if err != nil {
s.logger.Printf("Warning: failed to scan row: %v", err)
continue
}
if err := s.unmarshalRequestFields(&req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil {
s.logger.Printf("Warning: failed to unmarshal request fields: %v", err)
continue
}
requests = append(requests, req)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating rows: %w", err)
}
return requests, nil
}
// scanSingleRow scans a single row into a RequestLog pointer
func (s *sqliteStorageService) scanSingleRow(rows *sql.Rows) (*model.RequestLog, error) {
var req model.RequestLog
var headersJSON, bodyJSON string
var promptGradeJSON, responseJSON sql.NullString
err := rows.Scan(
&req.RequestID,
&req.Timestamp,
&req.Method,
&req.Endpoint,
&headersJSON,
&bodyJSON,
&req.Model,
&req.UserAgent,
&req.ContentType,
&promptGradeJSON,
&responseJSON,
&req.OriginalModel,
&req.RoutedModel,
)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
if err := s.unmarshalRequestFields(&req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil {
return nil, err
}
return &req, nil
}
// unmarshalRequestFields unmarshals JSON fields into a RequestLog
func (s *sqliteStorageService) unmarshalRequestFields(req *model.RequestLog, headersJSON, bodyJSON string, promptGradeJSON, responseJSON sql.NullString) error {
if err := json.Unmarshal([]byte(headersJSON), &req.Headers); err != nil {
return fmt.Errorf("failed to unmarshal headers: %w", err)
}
var body interface{}
if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {
return fmt.Errorf("failed to unmarshal body: %w", err)
}
req.Body = body
if promptGradeJSON.Valid {
var grade model.PromptGrade
if err := json.Unmarshal([]byte(promptGradeJSON.String), &grade); err != nil {
s.logger.Printf("Warning: failed to unmarshal prompt grade: %v", err)
} else {
req.PromptGrade = &grade
}
}
if responseJSON.Valid {
var resp model.ResponseLog
if err := json.Unmarshal([]byte(responseJSON.String), &resp); err != nil {
s.logger.Printf("Warning: failed to unmarshal response: %v", err)
} else {
req.Response = &resp
}
}
return nil
}
func (s *sqliteStorageService) cleanupExpiredRequests() error {
if s.config == nil || s.config.RetentionDays <= 0 {
return nil
}
_, err := s.DeleteRequestsOlderThan(time.Duration(s.config.RetentionDays) * 24 * time.Hour)
return err
}
func (s *sqliteStorageService) prepareRequestBodyForStorage(body interface{}) (interface{}, error) {
if s.shouldSuppressBodies() {
return storageBodyPlaceholder("metadata_only"), nil
}
if s.config != nil && !s.config.CaptureRequestBody {
return storageBodyPlaceholder("request_body_disabled"), nil
}
normalized, err := normalizeJSONValue(body)
if err != nil {
return nil, err
}
fields := []string{}
if s.config != nil {
fields = s.config.RedactedFields
}
return redactJSONValue(normalized, redactedFieldSet(fields)), nil
}
func (s *sqliteStorageService) prepareResponseForStorage(response *model.ResponseLog) (*model.ResponseLog, error) {
if response == nil {
return nil, nil
}
clone := *response
if s.shouldSuppressBodies() || (s.config != nil && !s.config.CaptureResponseBody) {
clone.Body = nil
clone.BodyText = ""
clone.StreamingChunks = nil
return &clone, nil
}
if len(clone.Body) > 0 {
fields := []string{}
if s.config != nil {
fields = s.config.RedactedFields
}
sanitizedBody, err := sanitizeRawJSON(clone.Body, redactedFieldSet(fields))
if err != nil {
// Preserve the original payload if it cannot be parsed as JSON.
s.logger.Printf("Warning: failed to redact response body: %v", err)
} else {
clone.Body = sanitizedBody
}
}
return &clone, nil
}
func (s *sqliteStorageService) shouldSuppressBodies() bool {
return s.config != nil && s.config.MetadataOnly
}
func normalizeJSONValue(value interface{}) (interface{}, error) {
if value == nil {
return nil, nil
}
data, err := json.Marshal(value)
if err != nil {
return nil, err
}
var normalized interface{}
if err := json.Unmarshal(data, &normalized); err != nil {
return nil, err
}
return normalized, nil
}
func sanitizeRawJSON(raw json.RawMessage, redacted map[string]struct{}) (json.RawMessage, error) {
if len(raw) == 0 {
return raw, nil
}
var value interface{}
if err := json.Unmarshal(raw, &value); err != nil {
return raw, err
}
sanitized := redactJSONValue(value, redacted)
data, err := json.Marshal(sanitized)
if err != nil {
return raw, err
}
return json.RawMessage(data), nil
}
func redactJSONValue(value interface{}, redacted map[string]struct{}) interface{} {
switch typed := value.(type) {
case map[string]interface{}:
result := make(map[string]interface{}, len(typed))
for key, child := range typed {
if _, ok := redacted[strings.ToLower(key)]; ok {
result[key] = redactionPlaceholder
continue
}
result[key] = redactJSONValue(child, redacted)
}
return result
case []interface{}:
result := make([]interface{}, len(typed))
for i, child := range typed {
result[i] = redactJSONValue(child, redacted)
}
return result
default:
return value
}
}
func storageBodyPlaceholder(mode string) map[string]interface{} {
return map[string]interface{}{
"_storage_mode": mode,
}
}
func redactedFieldSet(fields []string) map[string]struct{} {
set := make(map[string]struct{}, len(fields))
for _, field := range fields {
field = strings.TrimSpace(strings.ToLower(field))
if field == "" {
continue
}
set[field] = struct{}{}
}
return set
}