698 lines
18 KiB
Go
698 lines
18 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
bodyJSON, err := json.Marshal(request.Body)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *sqliteStorageService) UpdateRequestWithResponse(request *model.RequestLog) error {
|
|
responseJSON, err := json.Marshal(request.Response)
|
|
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)
|
|
}
|
|
|
|
bodyJSON, err := json.Marshal(request.Body)
|
|
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 {
|
|
responseJSON, err := json.Marshal(request.Response)
|
|
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)
|
|
}
|
|
|
|
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
|
|
|
|
// 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
|
|
}
|