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
6 changes: 5 additions & 1 deletion deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ services:
UPDATE_AGENT_URL: "${UPDATE_AGENT_URL:-}"
UPDATE_AGENT_TOKEN: "${UPDATE_AGENT_TOKEN:-}"
UPDATE_GITHUB_TOKEN: "${UPDATE_GITHUB_TOKEN:-}"
# Request-log retention: default 7 days / 20k rows. Raise both when the
# usage/cost panel needs a longer history (~0.7 KB/row; ~150 MB at 90d @ 1.5k rows/day).
CLI2API_LOG_RETENTION_DAYS: "${CLI2API_LOG_RETENTION_DAYS:-7}"
CLI2API_LOG_MAX_ROWS: "${CLI2API_LOG_MAX_ROWS:-20000}"
volumes:
- qoder-data:/data
- "${CLI2API_UPDATER_SOCKET_DIR:-/tmp/cli2api-updater}:/run/cli2api-updater:ro"
Expand All @@ -33,4 +37,4 @@ services:
restart: unless-stopped

volumes:
qoder-data:
qoder-data:
37 changes: 36 additions & 1 deletion internal/logs/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,47 @@ import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"sync"
"time"

"github.com/caigee-cmd/cli2api/internal/accounts"
)

// Request-log retention. Defaults preserve the historical behaviour (7 days /
// 20k rows); deployments that want a longer usage history can raise the
// retention window and the row cap together.
const (
defaultRetentionDays = 7
defaultMaxLogRows = 20_000
retentionDaysEnv = "CLI2API_LOG_RETENTION_DAYS"
maxLogRowsEnv = "CLI2API_LOG_MAX_ROWS"
)

// RetentionDays is how long request logs are kept; 0 or an invalid value falls
// back to the default.
func RetentionDays() int {
if v := strings.TrimSpace(os.Getenv(retentionDaysEnv)); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
}
return defaultRetentionDays
}

// MaxLogRows caps the retained rows independently of the age window, so a burst
// of traffic cannot grow the SQLite file without bound.
func MaxLogRows() int {
if v := strings.TrimSpace(os.Getenv(maxLogRowsEnv)); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
}
return defaultMaxLogRows
}

// RequestPersister is the write surface the recorder actually uses.
// Query methods stay off this interface so a later store package can
// satisfy logs without logs depending on the concrete SQLite type.
Expand Down Expand Up @@ -200,7 +234,8 @@ func (r *RequestRecorder) PurgeLoop(stop <-chan struct{}, every time.Duration) {

func (r *RequestRecorder) purgeOnce() {
r.enqueue(func() {
if _, err := r.store.PurgeRequestLogs(context.Background(), 7*24*time.Hour, 20_000); err != nil {
keep := time.Duration(RetentionDays()) * 24 * time.Hour
if _, err := r.store.PurgeRequestLogs(context.Background(), keep, MaxLogRows()); err != nil {
logf("purge request logs: %v", err)
}
})
Expand Down
44 changes: 44 additions & 0 deletions internal/logs/retention_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package logs

import (
"testing"
)

func TestRetentionDaysDefaultsAndOverride(t *testing.T) {
t.Setenv(retentionDaysEnv, "")
if got := RetentionDays(); got != defaultRetentionDays {
t.Fatalf("default retention = %d, want %d", got, defaultRetentionDays)
}

t.Setenv(retentionDaysEnv, "90")
if got := RetentionDays(); got != 90 {
t.Fatalf("override retention = %d, want 90", got)
}

// Invalid values must not silently disable pruning.
for _, bad := range []string{"0", "-3", "abc", " "} {
t.Setenv(retentionDaysEnv, bad)
if got := RetentionDays(); got != defaultRetentionDays {
t.Fatalf("retention for %q = %d, want default %d", bad, got, defaultRetentionDays)
}
}
}

func TestMaxLogRowsDefaultsAndOverride(t *testing.T) {
t.Setenv(maxLogRowsEnv, "")
if got := MaxLogRows(); got != defaultMaxLogRows {
t.Fatalf("default max rows = %d, want %d", got, defaultMaxLogRows)
}

t.Setenv(maxLogRowsEnv, "200000")
if got := MaxLogRows(); got != 200000 {
t.Fatalf("override max rows = %d, want 200000", got)
}

for _, bad := range []string{"0", "-1", "lots"} {
t.Setenv(maxLogRowsEnv, bad)
if got := MaxLogRows(); got != defaultMaxLogRows {
t.Fatalf("max rows for %q = %d, want default %d", bad, got, defaultMaxLogRows)
}
}
}
Loading