Skip to content

Feature: Audit logs#33

Merged
SantiagoDePolonia merged 29 commits intomainfrom
feature/logs
Jan 16, 2026
Merged

Feature: Audit logs#33
SantiagoDePolonia merged 29 commits intomainfrom
feature/logs

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • Optional audit logging of requests/responses (including streaming usage) and provider identifiers on responses.
  • Configuration

    • New env/config options to choose storage backend (SQLite/Postgres/MongoDB) and control logging (enable, backend, bodies/headers, buffer, flush interval, retention, only-model-interactions).
  • Chores

    • Docker Compose updated to include Postgres, MongoDB and Adminer plus logging-related env vars.
  • Tests

    • Extensive unit and end-to-end tests for audit logging, redaction, streaming, and concurrency.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 14, 2026

Warning

Rate limit exceeded

@SantiagoDePolonia has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 23 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f4ebd2d and e2865c4.

📒 Files selected for processing (5)
  • Dockerfile
  • docker-compose.yaml
  • internal/auditlog/constants.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_sqlite_test.go

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a configurable audit-logging subsystem with pluggable storage backends (SQLite, PostgreSQL, MongoDB), async buffered logger, Echo middleware and streaming wrappers, storage abstraction and implementations, provider metadata propagation, docker-compose DB services, startup/init refactor, graceful shutdown, and extensive unit/e2e tests.

Changes

Cohort / File(s) Summary
Env & Config
\.env.template, \config/config.go``
Add storage and logging config structures, defaults, and env overrides (SQLITE_PATH, POSTGRES_URL/MAX_CONNS, MONGODB_URL/DB, LOGGING_*); add env helper readers and Logging.OnlyModelInteractions.
Bootstrap & Main
\cmd/gomodel/main.go`, `internal/providers/init.go`, `manifest_file``
Replace ad-hoc provider/cache init with providers.Init/InitResult; wire auditlog factory initialization; add graceful SIGINT/SIGTERM shutdown and include AuditLogger in server config.
Server wiring & handlers
\internal/server/http.go`, `internal/server/handlers.go`, `internal/server/handlers_test.go``
Add AuditLogger and LogOnlyModelInteractions; insert audit middleware before auth when enabled; centralize streaming handling via handleStreamingResponse; update NewHandler signature and tests.
Audit logging core
\internal/auditlog/*.go`, `internal/auditlog/auditlog_test.go``
New package: LogEntry/LogData types, redaction helpers, constants, async buffered Logger/NoopLogger/interface, Echo middleware, stream wrapper, enrichment APIs, and extensive unit tests.
Audit log stores
\internal/auditlog/store_sqlite.go`, `internal/auditlog/store_postgresql.go`, `internal/auditlog/store_mongodb.go``
Implement SQLite/Postgres/Mongo LogStore backends: schema/index creation, batch WriteBatch, retention cleanup, partial-write handling (Mongo), and lifecycle methods.
Audit factory
\internal/auditlog/factory.go`, `internal/auditlog/logger.go``
Initialize audit subsystem end-to-end: build storage, create LogStore, construct buffered Logger, and return Result with Close semantics.
Storage abstraction & impls
\internal/storage/storage.go`, `internal/storage/sqlite.go`, `internal/storage/postgresql.go`, `internal/storage/mongodb.go``
Add Storage interface, backend config types and New factory, per-backend constructors (NewSQLite, NewPostgreSQL, NewMongoDB) and accessors for DB/pool/client.
Provider & response plumbing
\internal/core/interfaces.go`, `internal/core/types.go`, `internal/core/responses.go`, `internal/providers/... ``
Add GetProviderType to RoutableProvider/Router; add Provider field to ChatResponse/ResponsesResponse; propagate provider metadata through provider implementations and responses stream converter (constructor now accepts provider).
Stream logging wrapper & middleware
\internal/auditlog/stream_wrapper.go`, `internal/auditlog/middleware.go`, `internal/auditlog/constants.go``
SSE stream wrapper captures usage and bodies; middleware captures request/response headers/bodies, decompression, redaction, enrichment helpers, and streaming support.
Tests & e2e
\tests/e2e/auditlog_test.go`, `internal/auditlog/auditlog_test.go`, `config/config_test.go`, `tests/e2e/main_test.go`, `internal/server/handlers_test.go``
Add extensive unit and e2e tests for middleware, redaction, streaming, concurrency, retention and LOGGING_ONLY_MODEL_INTERACTIONS; adjusted e2e networking to 127.0.0.1; updated tests for new handler signature.
Deployment & deps
\docker-compose.yaml`, `go.mod``
Add postgres, mongodb, adminer services/volumes; update gomodel env for DB and logging; add DB drivers and compression libs in go.mod.
Misc / tooling
\.claude/settings.local.json``
Expand local dev permission entries for build, lint, e2e, and docs commands.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Echo as "Echo Server"
    participant Audit as "Audit Middleware"
    participant Handler
    participant Logger
    participant Store
    participant DB

    Client->>Echo: HTTP request
    Echo->>Audit: middleware captures headers/body/API key hash
    Audit->>Handler: call downstream handler
    Handler-->>Audit: response (non-streaming)
    Audit->>Logger: Write(LogEntry) (async)
    Logger->>Logger: buffer / periodic flush
    Logger->>Store: WriteBatch(entries)
    Store->>DB: persist entries
    Audit-->>Client: HTTP response

    rect rgba(200,230,255,0.5)
      note right of Handler: Streaming flow
      Handler->>Audit: stream response (SSE)
      Audit->>Audit: wrap stream, accumulate usage
      Audit->>Logger: Write(stream LogEntry on close)
      Logger->>Store: WriteBatch(stream entries)
      Store->>DB: persist streaming entries
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hopped through headers, bodies, and streams,

I buffered each hop and kept careful beams.
Three cozy stores hold every little trace,
I hash secret keys and tuck logs in place.
A rabbit applauds — audits snug in their space!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature: Audit logs' accurately describes the main objective of the changeset, which introduces comprehensive audit logging functionality across the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

🤖 Fix all issues with AI agents
In `@cmd/gomodel/main.go`:
- Around line 244-247: Treat errors from srv.Start(addr) as fatal unless they
equal http.ErrServerClosed; update the handler that currently logs all errors
with slog.Info to check if err == http.ErrServerClosed then log an informational
message, otherwise log an error (slog.Error or slog.Log with level "error") and
exit with a nonzero status (e.g., os.Exit(1)); update imports to include
"net/http" and "os" and reference srv.Start and http.ErrServerClosed in the
check.
- Around line 280-322: The storage created by storage.New (variable store) is
closed only on error but never on the success path, leaking DB/connection
resources; fix by ensuring store.Close() is called when the app or audit logger
is shut down—either (a) change the audit log constructors
(auditlog.NewSQLiteStore, auditlog.NewPostgreSQLStore, auditlog.NewMongoDBStore)
to accept the original storage.Storage (or a close callback) so
auditLogger.Close can call store.Close, or (b) keep a reference to store in main
and defer or hook store.Close to run when auditLogger.Close is invoked; update
the creation path after switch (where logStore is set) to register/defer
store.Close() or pass store into the New*Store functions and ensure
auditLogger.Close triggers store.Close().

In `@config/config.go`:
- Around line 331-343: The existing expandEnvVars override block updates
LOGGING_ENABLED, LOGGING_STORAGE_TYPE, LOGGING_LOG_BODIES and
LOGGING_LOG_HEADERS but omits LOGGING_BUFFER_SIZE, LOGGING_FLUSH_INTERVAL and
LOGGING_RETENTION_DAYS; add env lookups for these three and set
cfg.Logging.BufferSize, cfg.Logging.FlushInterval and cfg.Logging.RetentionDays
respectively (parse integers with strconv.Atoi for BufferSize and RetentionDays,
and parse FlushInterval using time.ParseDuration or atoi+seconds depending on
the config field type), and handle parse errors by leaving the existing value
unchanged or logging the parse failure.

In `@docker-compose.yaml`:
- Around line 51-63: The mongodb service in docker-compose lacks authentication;
add MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD environment
variables to the mongodb service (and optionally MONGO_INITDB_DATABASE) and
ensure they are sourced securely (e.g., from an env_file or Docker secrets
rather than hardcoding); then update the gomodel service connection URL to use
credentials (e.g.,
mongodb://<username>:<password>@mongodb:27017/?authSource=admin) so the app uses
the root user for initial connection; reference the mongodb service block and
the gomodel connection config when making these changes.

In `@internal/auditlog/auditlog_test.go`:
- Around line 235-241: Replace the flaky time.Sleep in the test by adding a
synchronization helper on mockStore (e.g., add a method WaitForEntries(count
int, timeout time.Duration) bool) that checks store.entries under its mutex
(mockStore.mu) and waits until the desired count is reached (using a loop with
short sleeps or a condition variable/chan) and then use require.True(t,
store.WaitForEntries(5, 500*time.Millisecond)) in place of the Sleep/len
assertion; ensure the WaitForEntries method returns false on timeout and the
test asserts accordingly.

In `@internal/auditlog/auditlog.go`:
- Around line 94-117: RedactHeaders currently does O(n×m) work by iterating
RedactedHeaders for each header; convert RedactedHeaders into a package-level
set (e.g., redactedHeadersSet) built once (via init or a var with a closure)
mapping lowercase header names to struct{}, then update RedactHeaders to
lower-case each key and test membership with a single map lookup (if _, ok :=
redactedHeadersSet[strings.ToLower(key)]; ok) to assign "[REDACTED]" or the
original value accordingly, preserving the nil check and result map sizing.

In `@internal/auditlog/logger.go`:
- Around line 49-63: Logger.Write can panic when logging a dropped entry because
it directly accesses entry.Data.RequestID; update Logger.Write to guard against
nil entry.Data before using its fields (e.g., check if entry.Data != nil and use
entry.Data.RequestID, otherwise use an empty/placeholder value like "unknown" or
omit the field) when calling slog.Warn so dropped-entry logging cannot cause a
nil pointer dereference; reference the Logger.Write method and the LogEntry
struct/entry.Data.RequestID to locate and fix the code.
- Around line 95-101: The batch slice is being reused with batch = batch[:0],
causing backing-array aliasing and corrupting data observed by WriteBatch; fix
by ensuring a fresh slice is passed to the flusher (e.g., call
l.flushBatch(append([]Entry(nil), batch...)) or have flushBatch/called function
itself copy the slice into a new slice before storing/async use), and stop
reusing the same backing array (alternatively reset with batch = make([]Entry,
0, 100) if you need reserved capacity).

In `@internal/auditlog/middleware.go`:
- Around line 169-190: EnrichEntry currently dereferences entry.Data without a
nil check (unlike EnrichEntryWithError/EnrichEntryWithStream); fix EnrichEntry
by checking if entry.Data is nil and initializing it before assigning tokens
(e.g., if entry.Data == nil { entry.Data = &LogData{} } or the correct
zero-value struct for your LogEntry.Data type), then set
entry.Data.PromptTokens, CompletionTokens, and TotalTokens as before.
- Around line 118-131: The responseBodyCapture wrapper currently only implements
Write and thus does not satisfy http.Flusher, breaking SSE streaming; add a
Flush method on responseBodyCapture that checks if r.ResponseWriter implements
http.Flusher and if so calls its Flush(), otherwise no-op, so handlers can
safely call Flush(); update the type to satisfy the Flusher interface by
delegating to the underlying ResponseWriter (and similarly consider delegating
other optional interfaces like http.Hijacker or http.CloseNotifier if needed
elsewhere).
- Around line 133-153: The two identical functions extractHeaders and
extractEchoHeaders should be consolidated into a single shared function (e.g.,
extractHeaders) that takes map[string][]string, builds the map[string]string by
taking the first value for each key, and then calls RedactHeaders; remove the
duplicate extractEchoHeaders and update any callers that reference
extractEchoHeaders to call the consolidated extractHeaders instead (or add a
thin wrapper forwarding to the single implementation if API compatibility is
required).

In `@internal/auditlog/store_mongodb.go`:
- Around line 49-56: Compute the TTL using 64-bit arithmetic and clamp to int32
before casting to avoid overflow: convert retentionDays to int64 and multiply
(int64(retentionDays) * 24 * 60 * 60), clamp the result to math.MaxInt32 if it
exceeds that, then cast to int32 and use that value for
options.Index().SetExpireAfterSeconds when building the mongo.IndexModel for the
"timestamp" key.

In `@internal/auditlog/store_postgresql.go`:
- Around line 117-118: Remove the dead placeholder by deleting the lines that
create and ignore the variable "batch := &pgxpool.Pool{}" and "_ = batch" in
store_postgresql.go; ensure no remaining references to "batch" exist and run the
linter/tests to confirm no unused-variable warnings remain.

In `@internal/auditlog/store_sqlite.go`:
- Around line 122-129: The Close method on SQLiteStore can panic if called twice
because it unconditionally closes the stopCleanup channel; modify
SQLiteStore.Close to guard the close with a sync.Once (e.g., add a field like
cleanupOnce sync.Once) and/or check for nil, and call cleanupOnce.Do(func(){
close(s.stopCleanup) }) only when s.retentionDays > 0 and s.stopCleanup != nil
so repeated Close() calls are safe; keep returning nil as before.

In `@internal/auditlog/stream_wrapper.go`:
- Around line 101-138: The extractUsageFromJSON function currently unmarshals
into map[string]interface{}; either document why a dynamic map is required
(responses vary by provider) or replace it with a minimal strongly-typed wrapper
to make intent clear and simplify parsing: define a chunkWithUsage struct with a
Usage *Usage `json:"usage"` field and update extractUsageFromJSON to unmarshal
into that struct, return chunk.Usage when non-nil and has token counts > 0; if
you keep the map approach add a short comment above extractUsageFromJSON
explaining the multi-provider rationale.
- Around line 232-245: Replace the hardcoded list inside SkipLoggingPaths with a
package-level variable (e.g. DefaultSkipPaths []string) and have
SkipLoggingPaths iterate over that slice; optionally add a setter or read from
the audit logging config at startup (e.g. SetSkipPaths or initialize
DefaultSkipPaths from AuditConfig.SkipPaths) so callers/tests can modify the
skip list. Update references to SkipLoggingPaths only to use the new package var
and/or setter so the skip paths become configurable without changing the
function logic.
- Around line 149-185: CreateStreamEntry currently shallow-copies pointer fields
Temperature and MaxTokens from baseEntry.Data into the new LogData, so the
original and copied entries share underlying pointers; update CreateStreamEntry
to deep-copy those pointers (check for nil and allocate new values) instead of
assigning them directly by adding helper functions (e.g., copyFloat64Ptr for
Temperature and copyIntPtr for MaxTokens) and using those helpers when
constructing the LogData in CreateStreamEntry.

In `@internal/storage/mongodb.go`:
- Around line 70-75: The Close method currently calls s.client.Disconnect with
context.Background(), which can block indefinitely; update mongoStorage.Close to
create a timeout context (e.g., context.WithTimeout) with a short configurable
duration (e.g., 5s or a constant) and call s.client.Disconnect(ctx) then cancel
the context, returning any error from Disconnect; ensure the context is used
only for the disconnect call and that s.client nil-check remains.

In `@internal/storage/postgresql.go`:
- Around line 74-77: The comment should clarify intended usage: update the doc
comment for the Pool() method on postgresStorage to state it is an internal
convenience that returns the underlying *pgxpool.Pool and that consumers should
prefer the Storage.PostgreSQLPool() accessor on the interface rather than
type-asserting to *postgresStorage unless they need direct pgx access; also
update the PostgreSQLPool() documentation to reference that it is the public way
to obtain the pool and mention when a type assertion to *postgresStorage/Pool()
is acceptable.

In `@internal/storage/sqlite.go`:
- Around line 38-42: The comment for the SQLite connection pool is misleading:
db.SetMaxOpenConns(1) and db.SetMaxIdleConns(1) enforce a single connection and
therefore do not permit concurrent reads; either update the comment to
accurately state that a single connection is used (one writer and no concurrent
read connections) or change the pool setting (e.g., increase the value passed to
db.SetMaxOpenConns) if you intentionally want concurrent reads (ensure WAL mode
and test concurrency). Locate the calls db.SetMaxOpenConns and
db.SetMaxIdleConns in sqlite.go and either adjust the comment to reflect the
single-connection design or increase MaxOpenConns to a suitable number when
enabling concurrent reads.

In `@internal/storage/storage.go`:
- Around line 56-78: The Storage interface returns interface{} for
PostgreSQLPool() and MongoDatabase(), forcing callers to type-assert; fix by
adding typed accessor methods on the concrete storage implementations (e.g., add
PostgresPool() that returns *pgxpool.Pool on the Postgres storage struct and add
Database() or Client() that returns *mongo.Database on the Mongo storage struct,
similar to the existing Database()/Client() in mongodb.go) and keep the current
interface methods for compatibility; update documentation and callers to
type-assert the Storage value to the concrete type (e.g.,
storage.(*postgresStorage).PostgresPool() or storage.(*mongoStorage).Database())
so callers can obtain the concrete typed objects without asserting interface{}.

In `@tests/e2e/auditlog_test.go`:
- Around line 465-467: Tighten the tolerance on the audit log assertion: replace
the current check that allows two missing entries (the call to
store.WaitForAPIEntries(numRequests, 5*time.Second) and the
assert.GreaterOrEqual using numRequests-2) with an assertion that requires all
entries to be present (compare against numRequests directly, e.g.,
assert.GreaterOrEqual/ assert.Equal against numRequests or assert.Len on entries
to equal numRequests) so any dropped or missing logs are surfaced during the
test.
- Around line 557-560: The test builds the request payload as a
map[string]interface{} (payload) but must use the strongly-typed struct
core.ChatRequest per guidelines; replace the map with an instance of
core.ChatRequest (set Model to "unsupported-model-xyz" and populate Messages
with the appropriate slice of core.ChatMessage or core.ChatRequest.Message type
entries for role "user" and content "Hello"), then marshal that struct when
sending the request so the test uses the typed request object instead of a map.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c714894 and 117e228.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (19)
  • .env.template
  • cmd/gomodel/main.go
  • config/config.go
  • docker-compose.yaml
  • go.mod
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/stream_wrapper.go
  • internal/server/http.go
  • internal/storage/mongodb.go
  • internal/storage/postgresql.go
  • internal/storage/sqlite.go
  • internal/storage/storage.go
  • tests/e2e/auditlog_test.go
🧰 Additional context used
📓 Path-based instructions (7)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/server/http.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/auditlog.go
  • tests/e2e/auditlog_test.go
  • internal/storage/postgresql.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/middleware.go
  • internal/storage/sqlite.go
  • internal/auditlog/auditlog_test.go
  • cmd/gomodel/main.go
  • config/config.go
  • internal/auditlog/store_postgresql.go
  • internal/storage/storage.go
  • internal/storage/mongodb.go
  • internal/auditlog/logger.go
  • internal/auditlog/stream_wrapper.go
internal/server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use Echo (v4) framework for HTTP server setup and middleware, not Chi or other frameworks

Files:

  • internal/server/http.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • tests/e2e/auditlog_test.go
  • internal/auditlog/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/auditlog_test.go
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
config/config.go

📄 CodeRabbit inference engine (CLAUDE.md)

config/config.go: Use Viper for loading configuration from environment variables and .env files in the config package
At least one provider API key is required in the environment configuration to start the server

Files:

  • config/config.go
go.mod

📄 CodeRabbit inference engine (CLAUDE.md)

Go version must be 1.24.0 or later

Files:

  • go.mod
🧠 Learnings (9)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/server/http.go
  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : At least one provider API key is required in the environment configuration to start the server

Applied to files:

  • .env.template
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/auditlog_test.go
  • go.mod
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • tests/e2e/auditlog_test.go
  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/cache/**/*.go : Cache structure must use `ModelCache` struct with Version, UpdatedAt, and Models (map[string]CachedModel) fields

Applied to files:

  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : Use `Viper` for loading configuration from environment variables and .env files in the config package

Applied to files:

  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to go.mod : Go version must be 1.24.0 or later

Applied to files:

  • go.mod
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/auditlog/stream_wrapper.go
🧬 Code graph analysis (12)
internal/server/http.go (3)
internal/auditlog/logger.go (1)
  • LoggerInterface (165-169)
internal/auditlog/auditlog.go (1)
  • Config (120-138)
internal/auditlog/middleware.go (1)
  • Middleware (27-116)
internal/auditlog/store_sqlite.go (1)
internal/auditlog/auditlog.go (1)
  • LogEntry (29-46)
tests/e2e/auditlog_test.go (5)
internal/auditlog/auditlog.go (2)
  • LogEntry (29-46)
  • Config (120-138)
internal/server/http.go (3)
  • Config (25-31)
  • Server (19-22)
  • New (34-96)
internal/auditlog/logger.go (2)
  • Logger (13-20)
  • NewLogger (24-44)
internal/providers/router.go (1)
  • NewRouter (25-32)
internal/core/types.go (3)
  • ChatRequest (4-10)
  • Model (55-60)
  • Message (25-28)
internal/storage/postgresql.go (1)
internal/storage/storage.go (3)
  • PostgreSQLConfig (41-46)
  • Storage (58-78)
  • TypePostgreSQL (15-15)
internal/auditlog/store_mongodb.go (1)
internal/auditlog/auditlog.go (1)
  • LogEntry (29-46)
internal/storage/sqlite.go (1)
internal/storage/storage.go (3)
  • SQLiteConfig (35-38)
  • Storage (58-78)
  • TypeSQLite (14-14)
internal/auditlog/auditlog_test.go (3)
internal/auditlog/auditlog.go (4)
  • RedactHeaders (96-117)
  • LogEntry (29-46)
  • LogData (50-79)
  • Config (120-138)
internal/auditlog/stream_wrapper.go (4)
  • SkipLoggingPaths (233-245)
  • NewStreamLogWrapper (23-29)
  • WrapStreamForLogging (142-147)
  • CreateStreamEntry (151-185)
internal/auditlog/middleware.go (1)
  • Usage (224-228)
config/config.go (1)
internal/storage/storage.go (1)
  • Storage (58-78)
internal/auditlog/store_postgresql.go (1)
internal/auditlog/auditlog.go (1)
  • LogEntry (29-46)
internal/storage/storage.go (4)
config/config.go (1)
  • Config (26-33)
internal/storage/sqlite.go (1)
  • NewSQLite (19-51)
internal/storage/postgresql.go (1)
  • NewPostgreSQL (18-49)
internal/storage/mongodb.go (1)
  • NewMongoDB (19-52)
internal/auditlog/logger.go (2)
internal/auditlog/auditlog.go (3)
  • LogStore (14-25)
  • Config (120-138)
  • LogEntry (29-46)
internal/server/http.go (1)
  • Config (25-31)
internal/auditlog/stream_wrapper.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (165-169)
internal/auditlog/auditlog.go (3)
  • LogEntry (29-46)
  • Config (120-138)
  • LogData (50-79)
internal/auditlog/middleware.go (2)
  • Usage (224-228)
  • LogEntryKey (21-21)
internal/server/http.go (1)
  • Config (25-31)
🪛 ast-grep (0.40.5)
tests/e2e/auditlog_test.go

[warning] 111-111: "Detected a network listener listening on 0.0.0.0 or an empty string.
This could unexpectedly expose the server publicly as it binds to all
available interfaces. Instead, specify another IP address that is not
0.0.0.0 nor the empty string."
Context: net.Listen("tcp", ":0")
Note: [CWE-200] Exposure of Sensitive Information to an Unauthorized Actor [REFERENCES]
- https://owasp.org/Top10/A01_2021-Broken_Access_Control

(avoid-bind-to-all-interfaces-go)

🪛 Checkov (3.2.334)
.env.template

[medium] 40-41: Basic Auth Credentials

(CKV_SECRET_4)

🪛 GitHub Actions: CI
internal/auditlog/auditlog_test.go

[error] 239-239: race detected during execution of TestLogger


[error] 449-449: race detected during execution of TestStreamLogWrapper

internal/auditlog/logger.go

[error] 140-140: Race observed in flushBatch/flushLoop leading to data race when multiple goroutines access mockStore.WriteBatch

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (40)
.env.template (2)

32-46: LGTM! Storage configuration section is well-documented.

The storage backend options (SQLite, PostgreSQL, MongoDB) are clearly documented with example connection strings. The Checkov warning (CKV_SECRET_4) for the PostgreSQL URL is a false positive—this is a template file with placeholder credentials, not actual secrets.


47-75: Good documentation of audit logging options with appropriate security warnings.

The configuration includes proper warnings about PII and sensitive data when LOGGING_LOG_BODIES is enabled, and notes that sensitive headers are automatically redacted when LOGGING_LOG_HEADERS is enabled. The buffer size, flush interval, and retention settings provide good operational controls.

internal/storage/storage.go (1)

80-109: LGTM! Factory function and defaults are well-structured.

The New() factory properly validates the storage type and delegates to backend-specific constructors. The error message helpfully lists valid options. DefaultConfig() provides sensible defaults that align with the .env.template documentation.

internal/storage/sqlite.go (1)

17-51: LGTM! Solid SQLite initialization with WAL mode.

The implementation correctly:

  • Creates parent directories if needed
  • Enables WAL mode for better concurrency characteristics
  • Sets a 5-second busy timeout to handle lock contention
  • Uses NORMAL synchronous mode for a good durability/performance balance
  • Properly cleans up on connection failure
internal/storage/mongodb.go (2)

18-52: LGTM! Well-structured MongoDB initialization with proper validation.

The implementation correctly:

  • Validates that URL is provided
  • Falls back to default database name "gomodel"
  • Verifies connectivity with ping before returning
  • Properly disconnects on ping failure to avoid leaking connections

77-85: Good addition of typed accessors.

The Database() and Client() methods provide type-safe access to the underlying MongoDB resources without requiring callers to type-assert from interface{}. This is a useful pattern that could be replicated for the PostgreSQL storage.

go.mod (1)

7-15: Go version 1.24.0 meets the requirement and dependencies are appropriate.

The three new direct dependencies align well with the multi-backend storage feature: pgx/v5 for PostgreSQL, mongo-driver/v2 for MongoDB, and modernc.org/sqlite for SQLite.

internal/server/http.go (2)

71-74: LGTM! Audit middleware integration is well-structured.

The middleware is correctly placed before authentication to capture all requests (including auth failures), and the triple-check (cfg != nil && cfg.AuditLogger != nil && cfg.AuditLogger.Config().Enabled) provides robust defense-in-depth. Based on learnings, this correctly uses Echo v4 framework.


26-30: LGTM!

The AuditLogger field uses a well-defined interface type (LoggerInterface) rather than a generic interface{}, which aligns with the coding guidelines for strongly-typed definitions.

internal/storage/postgresql.go (1)

16-49: LGTM! PostgreSQL connection setup follows best practices.

The implementation correctly:

  • Validates the URL before parsing
  • Applies pool size configuration with a sensible default
  • Verifies connectivity with Ping and cleans up on failure
  • Properly wraps errors with context
internal/auditlog/store_sqlite.go (2)

71-115: LGTM! Batch insert implementation is efficient.

The batch insert with INSERT OR IGNORE handles duplicate IDs gracefully, and JSON marshaling with fallback to {} on error prevents data loss while logging warnings.


44-55: Good defensive approach to index creation.

Logging warnings for index creation failures while continuing operation is appropriate since indexes are optimizations, not correctness requirements. The table creation correctly fails fast on error.

internal/auditlog/middleware.go (1)

155-167: LGTM!

The API key hashing approach correctly avoids storing sensitive credentials while providing enough uniqueness (8 hex chars = 32 bits) for audit identification purposes.

tests/e2e/auditlog_test.go (4)

1-3: LGTM: Build tag correctly applied for E2E tests.

The //go:build e2e tag is properly placed, complying with the coding guidelines requiring E2E tests to use the -tags=e2e build tag.


26-105: LGTM: Thread-safe mock log store implementation.

The mock store correctly uses mutex protection for all operations and returns copies in GetEntries() to prevent race conditions. The polling-based wait helpers are appropriate for test synchronization.


107-158: LGTM: Well-structured test server setup.

The net.Listen("tcp", ":0") on line 112 is the standard Go pattern for allocating a random available port in tests. The static analysis warning about binding to all interfaces is a false positive in this context—:0 binds to localhost with an ephemeral port, which is appropriate for isolated test execution.


160-377: LGTM: Comprehensive middleware test coverage.

The test suite thoroughly covers:

  • Basic metadata capture (method, path, status, duration)
  • Request/response body logging
  • Header redaction
  • Disabled logging behavior
  • API key hashing

Resource cleanup is properly handled with deferred shutdown calls.

internal/auditlog/logger.go (3)

10-20: LGTM: Well-designed async buffered logger.

The Logger struct correctly uses a buffered channel for non-blocking writes and a sync.WaitGroup for graceful shutdown coordination.


22-44: LGTM: Proper initialization with sensible defaults.

The constructor correctly applies defaults for BufferSize and FlushInterval, and properly tracks the background goroutine with the WaitGroup before spawning it.


148-169: LGTM: Clean null object pattern implementation.

NoopLogger correctly implements LoggerInterface with no-op methods, returning Enabled: false from Config() to indicate the disabled state.

internal/auditlog/store_postgresql.go (3)

13-18: LGTM: Clean struct definition.

The PostgreSQLStore struct properly encapsulates the connection pool, retention configuration, and cleanup coordination channel.


20-72: LGTM: Robust store initialization.

The constructor properly validates input, creates the table schema with appropriate types (JSONB for flexible data), and sets up useful indexes. Non-fatal handling of index creation errors is appropriate since indexes may already exist.


169-208: LGTM: Well-implemented retention cleanup.

The cleanup loop runs hourly with an initial cleanup on startup, uses an appropriate 5-minute timeout for potentially slow DELETE operations, and logs meaningful information about deleted records.

internal/auditlog/store_mongodb.go (2)

14-18: LGTM: Clean struct definition.

The MongoDBStore struct appropriately stores the collection reference and retention configuration.


70-99: LGTM: Robust batch write with partial failure handling.

The implementation correctly uses unordered inserts for performance and gracefully handles BulkWriteException to allow partial successes. This is the appropriate pattern for audit logging where best-effort persistence is acceptable.

internal/auditlog/auditlog_test.go (3)

13-107: LGTM: Comprehensive header redaction tests.

The TestRedactHeaders function provides excellent coverage including nil input, empty maps, case-insensitive matching, and multiple sensitive headers. The table-driven approach is clean and extensible.


287-311: LGTM: Good coverage of skip paths.

The TestSkipLoggingPaths function covers health, metrics, favicon, and API paths appropriately.


531-589: LGTM: Thorough API key hashing tests.

The tests properly verify empty input handling, consistency (same input → same hash), and uniqueness (different inputs → different hashes).

cmd/gomodel/main.go (2)

4-34: LGTM on imports and provider registration.

The imports are well-organized, and provider packages are correctly imported with blank imports to trigger their init() registration as per coding guidelines.


196-213: LGTM on audit logger initialization.

Clean implementation with a no-op fallback when disabled, proper deferred cleanup, and good observability logging the configuration when enabled.

config/config.go (4)

35-66: LGTM on LogConfig struct.

Well-documented configuration struct with appropriate defaults and clear comments about sensitive data handling.


68-100: LGTM on StorageConfig types.

Clean, strongly-typed configuration structs for each storage backend as required by coding guidelines.


170-182: LGTM on storage and logging defaults.

Sensible default values that align with the documented defaults in the struct comments.


400-422: LGTM on helper functions.

Clean, focused utility functions that follow existing patterns in the codebase.

internal/auditlog/auditlog.go (3)

12-25: LGTM on LogStore interface.

Well-designed interface with clear documentation about concurrency safety requirements and method purposes.


27-79: LGTM on LogEntry and LogData structs.

Strongly-typed structs with appropriate JSON/BSON tags and omitempty for optional fields to save storage space. Good organization with clear comments grouping related fields.


119-150: LGTM on Config and DefaultConfig.

Clean configuration struct with sensible defaults that align with the documented values in config/config.go.

internal/auditlog/stream_wrapper.go (3)

10-29: LGTM on StreamLogWrapper structure.

Clean wrapper design that follows the io.ReadCloser interface pattern. The embedding of io.ReadCloser allows transparent proxying of reads.


31-46: LGTM on Read implementation.

Correct buffering logic with a reasonable 8KB cap. The implementation preserves the most recent data where usage information typically appears.


48-69: LGTM on Close implementation.

Proper double-close protection, clean usage extraction, and correct ordering of operations (parse → update → log → close underlying).

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +74 to +77
// Pool returns the underlying pgxpool.Pool for direct access
func (s *postgresStorage) Pool() *pgxpool.Pool {
return s.pool
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider documenting the intended usage of Pool() vs PostgreSQLPool().

The Pool() method provides direct typed access but isn't part of the Storage interface, requiring a type assertion to *postgresStorage. Document whether consumers should use PostgreSQLPool() (via interface) or type-assert to access Pool().

🤖 Prompt for AI Agents
In `@internal/storage/postgresql.go` around lines 74 - 77, The comment should
clarify intended usage: update the doc comment for the Pool() method on
postgresStorage to state it is an internal convenience that returns the
underlying *pgxpool.Pool and that consumers should prefer the
Storage.PostgreSQLPool() accessor on the interface rather than type-asserting to
*postgresStorage unless they need direct pgx access; also update the
PostgreSQLPool() documentation to reference that it is the public way to obtain
the pool and mention when a type assertion to *postgresStorage/Pool() is
acceptable.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/auditlog/auditlog_test.go`:
- Around line 245-251: The test's LogEntry ID generation uses
string(rune('0'+i)) producing single-character IDs like "0", "1"; update the
loop that calls logger.Write(&LogEntry{...}) to generate descriptive IDs using
fmt.Sprintf("entry-%d", i) instead of string(rune(...)), and add "fmt" to the
test imports so IDs become "entry-0", "entry-1", etc., improving readability and
debugability.
♻️ Duplicate comments (4)
internal/storage/postgresql.go (1)

79-82: Pool accessor provides typed convenience access.

The implementation is correct. Per the existing review comment, consider adding documentation clarifying when consumers should use this typed accessor (via type assertion) versus PostgreSQLPool() (via interface).

internal/auditlog/middleware.go (3)

70-78: Unbounded request body read could cause memory exhaustion.

The request body is read fully into memory without a size limit (unlike the response body which has a 1MB cap at line 130). Large request bodies could cause OOM issues.

🐛 Proposed fix to limit request body capture
 // Capture request body if enabled
-if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
-	bodyBytes, err := io.ReadAll(req.Body)
+const maxBodyCapture int64 = 1024 * 1024 // 1MB limit
+if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 && req.ContentLength <= maxBodyCapture {
+	bodyBytes, err := io.ReadAll(req.Body)
 	if err == nil {
 		entry.Data.RequestBody = bodyBytes
 		// Restore the body for the handler
 		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
 	}
 }

156-176: Duplicate functions: extractHeaders and extractEchoHeaders have identical implementations.

Both functions perform the exact same operation. Consolidate into a single function.

♻️ Proposed refactor
 // extractHeaders extracts headers from a header map (http.Header or echo response headers),
 // taking only the first value of each key and redacting sensitive headers.
 func extractHeaders(headers map[string][]string) map[string]string {
 	result := make(map[string]string, len(headers))
 	for key, values := range headers {
 		if len(values) > 0 {
 			result[key] = values[0]
 		}
 	}
 	return RedactHeaders(result)
 }
-
-// extractEchoHeaders extracts headers from echo's header map
-func extractEchoHeaders(headers map[string][]string) map[string]string {
-	result := make(map[string]string, len(headers))
-	for key, values := range headers {
-		if len(values) > 0 {
-			result[key] = values[0]
-		}
-	}
-	return RedactHeaders(result)
-}

Then update line 104:

-entry.Data.ResponseHeaders = extractEchoHeaders(c.Response().Header())
+entry.Data.ResponseHeaders = extractHeaders(c.Response().Header())

192-213: Missing nil check for entry.Data in EnrichEntry.

EnrichEntry accesses entry.Data.PromptTokens (line 209) without checking if entry.Data is nil, while EnrichEntryWithError and EnrichEntryWithStream include this check (lines 223, 239).

🐛 Proposed fix
 func EnrichEntry(c echo.Context, model, provider string, usage *Usage) {
 	entryVal := c.Get(string(LogEntryKey))
 	if entryVal == nil {
 		return
 	}

 	entry, ok := entryVal.(*LogEntry)
-	if !ok || entry == nil {
+	if !ok || entry == nil || entry.Data == nil {
 		return
 	}

 	entry.Model = model
 	entry.Provider = provider

 	if usage != nil {
 		entry.Data.PromptTokens = usage.PromptTokens
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 117e228 and 3a6a2cd.

📒 Files selected for processing (6)
  • .claude/settings.local.json
  • cmd/gomodel/main.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/auditlog/middleware.go
  • internal/storage/postgresql.go
🧰 Additional context used
📓 Path-based instructions (3)
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • cmd/gomodel/main.go
  • internal/auditlog/middleware.go
  • internal/storage/postgresql.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • internal/auditlog/auditlog_test.go
🧠 Learnings (7)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • cmd/gomodel/main.go
  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • internal/auditlog/auditlog_test.go
🧬 Code graph analysis (4)
internal/auditlog/middleware.go (3)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/auditlog.go (3)
  • Config (120-138)
  • LogEntry (29-46)
  • RedactHeaders (96-117)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/storage/postgresql.go (1)
internal/storage/storage.go (3)
  • PostgreSQLConfig (41-46)
  • Storage (58-78)
  • TypePostgreSQL (15-15)
internal/auditlog/auditlog_test.go (3)
internal/auditlog/auditlog.go (4)
  • RedactHeaders (96-117)
  • LogEntry (29-46)
  • LogData (50-79)
  • Config (120-138)
internal/auditlog/stream_wrapper.go (4)
  • SkipLoggingPaths (233-245)
  • NewStreamLogWrapper (23-29)
  • WrapStreamForLogging (142-147)
  • CreateStreamEntry (151-185)
internal/auditlog/middleware.go (1)
  • Usage (247-251)
internal/auditlog/logger.go (3)
internal/auditlog/auditlog.go (3)
  • LogStore (14-25)
  • Config (120-138)
  • LogEntry (29-46)
internal/server/http.go (1)
  • Config (25-31)
internal/core/types.go (1)
  • Model (55-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
🔇 Additional comments (25)
.claude/settings.local.json (1)

8-9: LGTM!

The addition of the Bash(go build:*) permission is appropriate for a PR introducing new Go modules (audit logging framework). This allows the Claude assistant to build the Go code during development and testing.

internal/storage/postgresql.go (4)

1-15: LGTM!

Package declaration, imports, and struct definition are clean and appropriate. The postgresStorage struct correctly holds a reference to *pgxpool.Pool for connection pooling.


31-39: Integer conversion is properly bounded.

The upper bound check at lines 33-35 correctly handles the conversion from int (potentially 64-bit) to int32 by capping at math.MaxInt32. This prevents overflow issues on 64-bit systems.


41-54: LGTM!

Good resource management pattern - the pool is properly closed on ping failure to prevent connection leaks. Error messages are appropriately wrapped with context.


56-77: LGTM!

Interface implementations correctly align with the Storage interface definition. The defensive nil check in Close() is good practice for safety.

internal/auditlog/auditlog_test.go (7)

196-231: LGTM! Thread-safety properly implemented in mockStore.

The mockStore now correctly uses sync.Mutex to protect concurrent access to entries and closed fields. The helper methods getEntries() and isClosed() provide safe read access from tests while the background flush goroutine writes. This addresses the previously flagged race condition.


14-108: LGTM! Comprehensive header redaction tests.

The TestRedactHeaders function provides excellent coverage including nil input, empty map, non-sensitive headers, single and multiple sensitive headers, and case-insensitive matching. The table-driven approach is clean and maintainable.


110-194: LGTM! JSON serialization tests are thorough.

TestLogEntryJSON and TestLogDataWithBodies properly verify round-trip JSON marshaling/unmarshaling, including nested structures and json.RawMessage body fields.


305-329: LGTM! Path skip logic tests are correct.

Tests correctly verify that /health, /metrics, and /favicon.ico paths (and their subpaths) are skipped, while API paths like /v1/chat/completions are not.


331-407: LGTM! SSE usage parsing tests cover key scenarios.

The tests properly validate parsing of usage data from SSE streams, including empty input, missing usage, and usage appearing in different positions within the stream.


489-547: LGTM! Stream entry deep copy verification.

The test correctly verifies that CreateStreamEntry performs a deep copy of headers (lines 543-546), preventing unintended mutations between the original and stream entries.


549-607: LGTM! API key hashing tests are thorough.

Tests cover edge cases (empty, "Bearer " only), valid tokens (with/without Bearer prefix), hash length validation (8 chars), consistency (same input → same hash), and collision resistance (different inputs → different hashes).

internal/auditlog/logger.go (5)

49-68: LGTM! Nil pointer dereference fix applied.

The Write method now correctly guards against nil entry.Data (lines 59-62) before accessing RequestID, preventing potential panics when logging dropped entries.


88-133: LGTM! Batch slice reuse race condition fixed.

The flushLoop now creates a new slice with make([]*LogEntry, 0, 100) (lines 104, 111) after each flush instead of reusing the backing array with batch[:0]. This prevents data corruption when WriteBatch stores or processes the slice asynchronously.


22-44: LGTM! Logger initialization is well-designed.

The constructor properly applies sensible defaults for BufferSize (1000) and FlushInterval (5s), starts the background flush goroutine with proper WaitGroup tracking, and returns a fully initialized logger ready for use.


75-86: LGTM! Clean shutdown implementation.

The Close method signals the flush loop via the done channel, waits for completion with WaitGroup, then closes the underlying store. This ensures all buffered entries are flushed before shutdown.


152-173: LGTM! NoopLogger and interface abstraction.

NoopLogger provides a proper stub implementation returning Enabled: false in Config(), allowing callers to check if logging is active. The LoggerInterface abstraction enables clean dependency injection and testing.

internal/auditlog/middleware.go (3)

137-154: LGTM! SSE streaming and WebSocket support implemented.

The responseBodyCapture now implements http.Flusher (lines 140-144) for SSE streaming support and http.Hijacker (lines 149-154) for WebSocket upgrades. Both properly delegate to the underlying ResponseWriter when supported.


26-118: Middleware structure is well-designed.

The middleware correctly:

  • Short-circuits when logging is disabled (lines 32-35)
  • Generates request IDs if not present (lines 42-45)
  • Captures comprehensive request metadata including hashed API key
  • Stores entry in context for handler enrichment (line 81)
  • Calculates duration and captures response metadata after handler execution
  • Writes entries asynchronously (line 113)

178-190: LGTM! API key hashing provides safe identification.

The hashAPIKey function correctly strips the "Bearer " prefix, handles empty tokens, and returns a truncated SHA256 hash (8 chars) for identification without exposing the actual key.

cmd/gomodel/main.go (5)

198-217: LGTM! Audit logger initialization with proper cleanup.

The audit logger setup correctly:

  • Returns both logger and storage for independent cleanup
  • Uses deferred closes in correct LIFO order (logger closes before storage)
  • Logs comprehensive configuration details when enabled

229-255: LGTM! Graceful shutdown and error handling fixed.

The shutdown handler properly:

  • Listens for SIGINT/SIGTERM signals
  • Uses a 30-second timeout for graceful shutdown
  • Distinguishes http.ErrServerClosed (graceful) from actual startup failures (fatal)

258-296: LGTM! Storage configuration with sensible defaults.

The initAuditLogger function properly:

  • Uses LOGGING_STORAGE_TYPE to select backend while using shared connection settings
  • Applies sensible defaults: SQLite type, .cache/gomodel.db path, gomodel database name
  • Creates storage connection with proper error wrapping

297-336: LGTM! Storage type switch with proper cleanup on errors.

Each storage type branch:

  • Properly validates the returned connection (nil checks)
  • Uses type assertions to get the concrete types needed by store constructors
  • Calls store.Close() on all error paths to prevent resource leaks

338-357: LGTM! Logger configuration with defaults.

The logger config properly applies defaults for BufferSize (1000) and FlushInterval (5s) when not specified, matching the defaults in NewLogger.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive audit logging functionality to the AI gateway, enabling request/response metadata capture with configurable storage backends (SQLite, PostgreSQL, MongoDB).

Changes:

  • Audit logging infrastructure with buffered async writes and configurable retention
  • Multi-backend storage abstraction layer supporting SQLite, PostgreSQL, and MongoDB
  • HTTP middleware for capturing request/response metadata with optional body/header logging and automatic redaction

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/auditlog/*.go Core audit logging implementation with logger, stores, middleware, and stream handling
internal/storage/*.go Storage abstraction layer for multi-backend database connections
internal/server/http.go Integration of audit logging middleware into HTTP server
config/config.go Configuration structs and environment variable handling for audit logging
cmd/gomodel/main.go Initialization logic for audit logger with graceful shutdown
docker-compose.yaml MongoDB service addition for audit log storage
tests/e2e/auditlog_test.go E2E tests for audit logging functionality
.env.template Documentation of audit logging configuration options

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if retentionDays > 0 {
ttlSeconds := int32(retentionDays * 24 * 60 * 60)
indexes = append(indexes, mongo.IndexModel{
Keys: bson.D{{Key: "timestamp", Value: 1}},
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MongoDB TTL index conflict: there are two indexes on timestamp (lines 36 descending, line 53 ascending for TTL). MongoDB doesn't allow multiple indexes on the same field with different directions when one is a TTL index. The TTL index should use the same direction (-1) as the query index, or the query index should be removed when TTL is enabled.

Suggested change
Keys: bson.D{{Key: "timestamp", Value: 1}},
Keys: bson.D{{Key: "timestamp", Value: -1}},

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +113
// Write log entry asynchronously
logger.Write(entry)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streaming requests are logged twice: once by the middleware and once by StreamLogWrapper. The middleware should check IsEntryMarkedAsStreaming and skip logging if true. Add check before logger.Write: if !IsEntryMarkedAsStreaming(c) { logger.Write(entry) }

Suggested change
// Write log entry asynchronously
logger.Write(entry)
// Write log entry asynchronously, skipping streaming requests which are logged elsewhere
if !IsEntryMarkedAsStreaming(c) {
logger.Write(entry)
}

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +208
defer auditStore.Close()
defer auditLogger.Close()
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource cleanup order is incorrect. The logger should be closed before the store to ensure all buffered entries are flushed to storage before the connection closes. Swap the order: defer logger.Close() first, then defer store.Close().

Suggested change
defer auditStore.Close()
defer auditLogger.Close()
defer auditLogger.Close()
defer auditStore.Close()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cmd/gomodel/main.go`:
- Around line 338-354: The code is redundantly applying defaults to the
auditlog.Config (logCfg) after construction; remove the manual fallback block
that sets BufferSize to 1000 and FlushInterval to 5*time.Second so the defaults
are handled only by auditlog.NewLogger (and the default values in config.go), or
if you want an explicit fallback here keep a brief comment referencing
auditlog.NewLogger and only set values when they are zero to avoid duplicating
logic; update the block around logCfg (the BufferSize/FlushInterval checks)
accordingly.

In `@internal/auditlog/store_mongodb.go`:
- Around line 88-99: The handler currently swallows partial BulkWriteException
failures by logging via slog.Warn and returning nil; change it to return a
distinguishable error (or wrap the original error) when a
mongo.BulkWriteException occurs so callers know the insert was only partial. In
the block that checks `if bulkErr, ok := err.(mongo.BulkWriteException); ok {
... }` replace the current `return nil` with returning a formatted error that
includes counts (e.g., number of write errors vs len(entries)) and the original
`bulkErr`, and optionally increment/expose a metric for partial failures there
so operators can detect data loss.
♻️ Duplicate comments (5)
internal/auditlog/middleware.go (3)

70-78: Unbounded request body read could cause memory exhaustion.

The request body is read fully into memory without a size limit (unlike the response body which has a 1MB cap at line 132). Large request bodies could cause OOM issues.

🐛 Proposed fix to limit request body capture
 // Capture request body if enabled
-if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
-	bodyBytes, err := io.ReadAll(req.Body)
+if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
+	// Limit request body capture to 1MB to prevent memory exhaustion
+	const maxBodyCapture int64 = 1024 * 1024
+	if req.ContentLength > maxBodyCapture {
+		// Skip capture for very large bodies
+		goto skipBodyCapture
+	}
+	bodyBytes, err := io.ReadAll(req.Body)
 	if err == nil {
 		entry.Data.RequestBody = bodyBytes
 		// Restore the body for the handler
 		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
 	}
 }
+skipBodyCapture:

Or simply check ContentLength before reading:

-if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
+const maxBodyCapture int64 = 1024 * 1024
+if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 && req.ContentLength <= maxBodyCapture {

158-178: Duplicate functions: extractHeaders and extractEchoHeaders have identical implementations.

Both functions perform the exact same operation. Consolidate into a single function.

♻️ Proposed refactor to remove duplication
-// extractHeaders extracts headers from http.Header, redacting sensitive ones
-func extractHeaders(headers map[string][]string) map[string]string {
-	result := make(map[string]string, len(headers))
-	for key, values := range headers {
-		if len(values) > 0 {
-			result[key] = values[0]
-		}
-	}
-	return RedactHeaders(result)
-}
-
-// extractEchoHeaders extracts headers from echo's header map
-func extractEchoHeaders(headers map[string][]string) map[string]string {
+// extractHeaders extracts headers from a header map (http.Header or echo response headers),
+// taking only the first value of each key and redacting sensitive headers.
+func extractHeaders(headers map[string][]string) map[string]string {
 	result := make(map[string]string, len(headers))
 	for key, values := range headers {
 		if len(values) > 0 {
 			result[key] = values[0]
 		}
 	}
 	return RedactHeaders(result)
 }

Then update line 104:

-entry.Data.ResponseHeaders = extractEchoHeaders(c.Response().Header())
+entry.Data.ResponseHeaders = extractHeaders(c.Response().Header())

196-215: Missing nil check for entry.Data in EnrichEntry.

EnrichEntry accesses entry.Data.PromptTokens etc. without checking if entry.Data is nil, while EnrichEntryWithError (line 225) and EnrichEntryWithStream (line 241) include this check.

🐛 Proposed fix
 func EnrichEntry(c echo.Context, model, provider string, usage *Usage) {
 	entryVal := c.Get(string(LogEntryKey))
 	if entryVal == nil {
 		return
 	}

 	entry, ok := entryVal.(*LogEntry)
-	if !ok || entry == nil {
+	if !ok || entry == nil || entry.Data == nil {
 		return
 	}

 	entry.Model = model
 	entry.Provider = provider

 	if usage != nil {
 		entry.Data.PromptTokens = usage.PromptTokens
internal/auditlog/store_postgresql.go (1)

79-87: Misleading comment: COPY is not actually used.

The comment states "Use COPY for better performance with large batches" but the implementation uses individual INSERT statements. Update the comment to reflect the actual approach.

♻️ Proposed comment fix
-	// Use COPY for better performance with large batches
-	// But for smaller batches, use a simple INSERT
+	// For small batches, use simple INSERTs
+	// For larger batches, wrap in a transaction for atomicity
 	if len(entries) < 10 {
 		return s.writeBatchSmall(ctx, entries)
 	}
config/config.go (1)

331-343: Incomplete environment variable override for logging configuration.

The expandEnvVars function overrides LOGGING_ENABLED, LOGGING_STORAGE_TYPE, LOGGING_LOG_BODIES, and LOGGING_LOG_HEADERS, but misses LOGGING_BUFFER_SIZE, LOGGING_FLUSH_INTERVAL, and LOGGING_RETENTION_DAYS. This creates inconsistency between config-file and no-config-file paths.

🐛 Proposed fix to add missing overrides
 	if logHeaders := os.Getenv("LOGGING_LOG_HEADERS"); logHeaders != "" {
 		cfg.Logging.LogHeaders = strings.EqualFold(logHeaders, "true") || logHeaders == "1"
 	}
+	if bufferSize := os.Getenv("LOGGING_BUFFER_SIZE"); bufferSize != "" {
+		if size, err := strconv.Atoi(bufferSize); err == nil {
+			cfg.Logging.BufferSize = size
+		}
+	}
+	if flushInterval := os.Getenv("LOGGING_FLUSH_INTERVAL"); flushInterval != "" {
+		if interval, err := strconv.Atoi(flushInterval); err == nil {
+			cfg.Logging.FlushInterval = interval
+		}
+	}
+	if retentionDays := os.Getenv("LOGGING_RETENTION_DAYS"); retentionDays != "" {
+		if days, err := strconv.Atoi(retentionDays); err == nil {
+			cfg.Logging.RetentionDays = days
+		}
+	}
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c082703 and f7ea695.

📒 Files selected for processing (5)
  • cmd/gomodel/main.go
  • config/config.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/store_postgresql.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
  • config/config.go
  • cmd/gomodel/main.go
config/config.go

📄 CodeRabbit inference engine (CLAUDE.md)

config/config.go: Use Viper for loading configuration from environment variables and .env files in the config package
At least one provider API key is required in the environment configuration to start the server

Files:

  • config/config.go
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
🧠 Learnings (8)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/cache/**/*.go : Cache structure must use `ModelCache` struct with Version, UpdatedAt, and Models (map[string]CachedModel) fields

Applied to files:

  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : Use `Viper` for loading configuration from environment variables and .env files in the config package

Applied to files:

  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles

Applied to files:

  • cmd/gomodel/main.go
🧬 Code graph analysis (2)
config/config.go (1)
internal/storage/storage.go (1)
  • Storage (58-78)
cmd/gomodel/main.go (6)
internal/auditlog/logger.go (3)
  • LoggerInterface (169-173)
  • NoopLogger (153-153)
  • NewLogger (24-44)
internal/storage/storage.go (9)
  • Storage (58-78)
  • Config (20-32)
  • New (82-93)
  • SQLiteConfig (35-38)
  • PostgreSQLConfig (41-46)
  • MongoDBConfig (49-54)
  • TypeSQLite (14-14)
  • TypePostgreSQL (15-15)
  • TypeMongoDB (16-16)
config/config.go (1)
  • Config (26-33)
internal/auditlog/auditlog.go (2)
  • Config (120-138)
  • LogStore (14-25)
internal/server/http.go (3)
  • Config (25-31)
  • Server (19-22)
  • New (34-96)
internal/auditlog/store_sqlite.go (1)
  • NewSQLiteStore (23-69)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (23)
internal/auditlog/store_mongodb.go (4)

14-18: LGTM on struct definition.

The MongoDBStore struct is well-defined with appropriate fields for the collection reference and retention configuration.


23-71: Constructor implementation is solid.

The constructor properly validates input, creates appropriate indexes (including TTL index with correct descending direction for query performance), and handles index creation failures gracefully by logging warnings without failing initialization.


104-112: No-op implementations are appropriate.

The Flush and Close methods are correctly implemented as no-ops since MongoDB writes are synchronous and the client lifecycle is managed externally by the storage layer.


79-83: Consider using a typed slice instead of []interface{}.

Per coding guidelines, request/response payloads should use strongly-typed structs instead of interface{} or map[string]interface{}. The MongoDB driver's InsertMany accepts []interface{}, but you could use []any (Go 1.18+) or keep the current approach since this is an internal implementation detail driven by the driver API.

⛔ Skipped due to learnings
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
internal/auditlog/middleware.go (4)

29-120: Middleware structure and flow are well-designed.

The middleware correctly:

  • Skips logging when disabled (line 33-35)
  • Generates request IDs (lines 42-45)
  • Hashes API keys for safe logging (lines 61-63)
  • Delegates streaming request logging to StreamLogWrapper (line 113)
  • Properly captures response metadata

Based on learnings, the Echo v4 framework usage is correct.


122-156: Response capture wrapper correctly implements required interfaces.

The responseBodyCapture properly implements:

  • Write with 1MB buffer limit (line 132)
  • Flush for SSE streaming support (lines 142-146)
  • Hijack for WebSocket upgrades (lines 151-156)

This addresses the previous concern about SSE streaming.


180-192: API key hashing implementation is secure.

The hashAPIKey function properly extracts the token from the Bearer header and creates an 8-character SHA256 hash prefix for identification without exposing the actual key.


248-253: Usage struct is well-defined.

Clean struct for token usage tracking.

internal/auditlog/store_postgresql.go (5)

13-18: PostgreSQLStore struct is well-designed.

Clean struct with pool reference, retention configuration, and cleanup channel for graceful shutdown.


23-72: Constructor properly initializes table and indexes.

The constructor:

  • Validates the pool
  • Creates the table with appropriate schema including JSONB for flexible data storage
  • Creates useful indexes including a GIN index for JSONB queries
  • Starts background cleanup when retention is configured

115-150: Large batch implementation uses transactions appropriately.

The writeBatchLarge function correctly:

  • Begins a transaction
  • Defers rollback (which is a no-op if committed)
  • Handles individual insert errors gracefully
  • Commits at the end

However, for true batch performance with large entry counts, consider using pgx's CopyFrom in the future.


166-205: Cleanup loop implementation is correct.

The cleanup logic:

  • Runs initial cleanup immediately
  • Uses a ticker for hourly cleanup
  • Responds to stop signals for graceful shutdown
  • Uses a 5-minute timeout context to prevent long-running deletes
  • Logs cleanup results appropriately

152-164: Flush and Close implementations are appropriate.

Flush is correctly a no-op for synchronous PostgreSQL writes. Close properly signals the cleanup goroutine to stop.

config/config.go (5)

35-66: LogConfig struct is well-documented.

The struct has clear documentation for each field including defaults and warnings about sensitive data. The field naming is consistent (FlushInterval without "Seconds" suffix addresses the previous naming concern).


68-100: StorageConfig structs provide clean separation.

The nested storage configuration structs cleanly separate SQLite, PostgreSQL, and MongoDB settings with appropriate fields and documentation.


170-182: Default values are sensible.

The defaults for storage and logging are appropriate:

  • SQLite path: .cache/gomodel.db
  • PostgreSQL max_conns: 10
  • MongoDB database: gomodel
  • Logging buffer: 1000, flush interval: 5s, retention: 30 days

400-422: Helper functions are clean and reusable.

The getEnvOrDefault, getEnvIntOrDefault, and getEnvBool helper functions provide clean abstractions for environment variable parsing with sensible defaults. Based on coding guidelines, using Viper for configuration loading is the correct approach, and these helpers complement it for the no-config-file path.


213-234: Environment variable loading in no-config path is complete.

The no-config-file path correctly loads all storage and logging configuration from environment variables using the new helper functions.

cmd/gomodel/main.go (5)

4-36: Imports are properly organized.

The imports correctly include:

  • Standard library packages
  • External dependencies (pgxpool, mongo)
  • Internal packages with gomodel module prefix
  • Blank imports for provider registration (per coding guidelines)

198-217: Audit logger initialization and cleanup are correct.

The initialization:

  • Starts with a NoopLogger default
  • Only initializes storage when logging is enabled
  • Properly handles errors with early exit
  • Uses deferred cleanup in correct LIFO order (logger closes before store)

The defer order on lines 207-208 is correct: Go executes defers in LIFO order, so auditLogger.Close() (line 208) runs before auditStore.Close() (line 207), ensuring buffered entries are flushed before the storage connection closes.


229-243: Graceful shutdown implementation is robust.

The shutdown handler:

  • Listens for SIGINT and SIGTERM
  • Uses a buffered channel (capacity 1) to avoid blocking
  • Provides a 30-second timeout for graceful shutdown
  • Logs shutdown errors appropriately

248-255: Server startup error handling correctly distinguishes errors.

The error handling now properly:

  • Treats http.ErrServerClosed as graceful shutdown (info log)
  • Treats other errors as fatal (error log + exit)

This addresses the previous review concern.


258-357: initAuditLogger function is well-structured.

The function:

  • Creates storage configuration from the appropriate config sections
  • Applies sensible defaults
  • Properly handles all storage backends (SQLite, PostgreSQL, MongoDB)
  • Has comprehensive error handling with cleanup on failure paths
  • Returns both the logger and storage for proper lifecycle management

The type assertions for PostgreSQL pool and MongoDB database are necessary due to the interface{} return types in storage.Storage (to avoid import cycles, as noted in the relevant code snippets).

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

cfg := logger.Config()
start := time.Now()
req := c.Request()

Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middleware does not skip logging for paths like /health, /metrics, and /favicon.ico even though a SkipLoggingPaths function exists in stream_wrapper.go. This could result in excessive audit logs for health check and monitoring endpoints. Consider adding a check at the start of the middleware: if SkipLoggingPaths(req.URL.Path) { return next(c) }

Suggested change
// Skip logging for specific paths (e.g., /health, /metrics, /favicon.ico)
if SkipLoggingPaths(req.URL.Path) {
return next(c)
}

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +40
// Use a single connection to serialize all database access.
// This prevents "database is locked" errors at the cost of no concurrent reads.
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting MaxOpenConns to 1 serializes all database access, which may become a performance bottleneck under high load. While the comment mentions WAL mode for concurrent read/write performance, WAL mode's benefits are negated by limiting connections to 1. Consider increasing MaxOpenConns (e.g., to 5-10) to allow concurrent reads while still preventing lock contention, or document why this extreme serialization is necessary.

Suggested change
// Use a single connection to serialize all database access.
// This prevents "database is locked" errors at the cost of no concurrent reads.
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
// Limit the number of concurrent connections instead of fully serializing access.
// This reduces "database is locked" errors while still allowing concurrent reads in WAL mode.
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +36
if cfg.MaxConns > 0 {
maxConns := cfg.MaxConns
if maxConns > math.MaxInt32 {
maxConns = math.MaxInt32
}
poolCfg.MaxConns = int32(maxConns)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overflow check for MaxConns > math.MaxInt32 is unlikely to be needed in practice (2.1 billion connections). This check adds unnecessary complexity. Consider removing it or documenting a more realistic upper bound (e.g., 1000).

Copilot uses AI. Check for mistakes.
config/config.go Outdated
LogBodies: getEnvBool("LOGGING_LOG_BODIES"),
LogHeaders: getEnvBool("LOGGING_LOG_HEADERS"),
BufferSize: getEnvIntOrDefault("LOGGING_BUFFER_SIZE", 1000),
FlushInterval: getEnvIntOrDefault("LOGGING_FLUSH_INTERVAL", 5),
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FlushInterval field in LogConfig lacks a unit comment. The code shows it's used as seconds (line 344: time.Duration(cfg.Logging.FlushInterval) * time.Second), but the struct field comment on line 60 says 'in seconds' while the actual field doesn't indicate this. Consider renaming to FlushIntervalSeconds or adding a comment on the struct field to clarify units.

Suggested change
FlushInterval: getEnvIntOrDefault("LOGGING_FLUSH_INTERVAL", 5),
FlushInterval: getEnvIntOrDefault("LOGGING_FLUSH_INTERVAL", 5), // seconds

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +78
if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
bodyBytes, err := io.ReadAll(req.Body)
if err == nil {
entry.Data.RequestBody = bodyBytes
// Restore the body for the handler
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the entire request body into memory without size limits could lead to memory exhaustion attacks. Although there's a BodySizeLimit configured at the server level, it's safer to add an explicit check here or document the reliance on the server-level limit to prevent unbounded memory allocation.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +134
if r.body.Len() < 1024*1024 {
r.body.Write(b)
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 1024*1024 (1MB) for response body capture limit should be extracted as a named constant (e.g., MaxResponseBodyCaptureSize) to improve code clarity and maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +156
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 30; i++ {
resp, err := client.Get(serverURL + "/health")
if err == nil {
_ = resp.Body.Close()
if resp.StatusCode == http.StatusOK {
break
}
}
time.Sleep(100 * time.Millisecond)
}

Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server readiness check loops 30 times with no verification that the server is actually ready after the loop completes. If the server fails to start within 3 seconds, the test will continue and likely fail with confusing errors. Add a check after the loop to verify the server started successfully before proceeding with tests.

Suggested change
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 30; i++ {
resp, err := client.Get(serverURL + "/health")
if err == nil {
_ = resp.Body.Close()
if resp.StatusCode == http.StatusOK {
break
}
}
time.Sleep(100 * time.Millisecond)
}
client := &http.Client{Timeout: 2 * time.Second}
serverReady := false
for i := 0; i < 30; i++ {
resp, err := client.Get(serverURL + "/health")
if err == nil {
_ = resp.Body.Close()
if resp.StatusCode == http.StatusOK {
serverReady = true
break
}
}
time.Sleep(100 * time.Millisecond)
}
require.True(t, serverReady, "server did not become ready at %s within expected time", serverURL)

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +67
select {
case l.buffer <- entry:
// Entry queued successfully
default:
// Buffer full - drop entry and log warning
requestID := "unknown"
if entry.Data != nil {
requestID = entry.Data.RequestID
}
slog.Warn("audit log buffer full, dropping entry",
"request_id", requestID,
"model", entry.Model,
)
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently dropping audit log entries when the buffer is full may lead to compliance issues or missing critical audit trails. Consider adding a metric counter for dropped entries to make this observable, or document this behavior prominently in the configuration documentation to warn users about potential data loss.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +75
func (s *mongoStorage) Close() error {
if s.client != nil {
return s.client.Disconnect(context.Background())
}
return nil
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using context.Background() in Close() means the disconnect could hang indefinitely if MongoDB is unresponsive. Consider using a timeout context (e.g., context.WithTimeout with 10 seconds) to ensure the Close operation completes within a reasonable time during shutdown.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/auditlog/store_mongodb.go`:
- Around line 93-97: Change the blanket warning on index creation failures in
the collection.Indexes().CreateMany call to distinguish recoverable index
conflicts from fatal errors: when err is a mongo.CommandError (or unwraps to
one) check the Code (85 or 86) and only log a warning and continue for those
codes, but for any other error return the error so the caller fails fast; update
the error handling around CreateMany in store_mongodb.go (referencing
collection.Indexes().CreateMany and the local err variable) and import any
needed packages (e.g., the mongo driver error type and errors for unwrapping).
♻️ Duplicate comments (3)
internal/auditlog/store_mongodb.go (1)

78-91: TTL index configuration looks correct.

The implementation correctly uses a descending index (Value: -1) for the TTL index, and the conditional logic properly handles the case when retention is disabled. The single timestamp index approach avoids the MongoDB conflict between regular and TTL indexes.

The TTL calculation on line 82 could use int64 arithmetic before casting to be more defensive against overflow with very large retention values, but this was already noted in a previous review.

cmd/gomodel/main.go (2)

348-354: Duplicate defaults - consider removing for single source of truth.

These defaults (BufferSize: 1000, FlushInterval: 5s) are already applied in config.Load() and auditlog.NewLogger(). While not harmful, having defaults in three places creates maintenance burden.

This was flagged in a previous review as a nitpick.


207-208: Incorrect resource cleanup order - logger should close before storage.

Go's defer executes in LIFO (last-in-first-out) order. The current code registers auditLogger.Close() first (line 207) and auditStore.Close() second (line 208), meaning the storage will close before the logger.

This is problematic because auditLogger.Close() likely flushes buffered entries to the store. If the store is already closed, these writes will fail silently or error.

🐛 Proposed fix: Swap defer order
-		defer auditLogger.Close()
-		defer auditStore.Close()
+		defer auditStore.Close()   // Closes LAST (after logger flush)
+		defer auditLogger.Close()  // Closes FIRST (flushes to store)

With this order:

  1. auditLogger.Close() executes first → flushes buffered entries to storage
  2. auditStore.Close() executes second → safely closes the connection
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7ea695 and 4794932.

📒 Files selected for processing (3)
  • .claude/settings.local.json
  • cmd/gomodel/main.go
  • internal/auditlog/store_mongodb.go
🧰 Additional context used
📓 Path-based instructions (2)
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • cmd/gomodel/main.go
  • internal/auditlog/store_mongodb.go
🧠 Learnings (4)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles

Applied to files:

  • cmd/gomodel/main.go
🧬 Code graph analysis (1)
internal/auditlog/store_mongodb.go (3)
internal/server/http.go (1)
  • New (34-96)
internal/storage/storage.go (1)
  • New (82-93)
internal/auditlog/auditlog.go (1)
  • LogEntry (29-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: CodeQL analysis (go)
  • GitHub Check: Analyze (go)
🔇 Additional comments (8)
.claude/settings.local.json (1)

8-10: LGTM!

The added permissions for go build:* and golangci-lint run:* are appropriate for Go development workflows and align well with the audit logs feature being developed. These are standard, scoped commands with no security concerns.

internal/auditlog/store_mongodb.go (3)

17-36: LGTM! Well-designed error handling for partial writes.

The PartialWriteError type provides good context with TotalEntries, FailedCount, and the underlying Cause. The implementation of Unwrap() returning the sentinel ErrPartialWrite allows callers to use errors.Is() for easy detection.


105-143: LGTM! Solid batch write implementation with proper error handling.

The WriteBatch method correctly:

  • Uses unordered inserts for partial success scenarios
  • Detects BulkWriteException and returns a typed PartialWriteError
  • Increments the Prometheus counter for observability
  • Logs partial failures with useful context

The []interface{} slice on line 112 is required by the MongoDB driver's InsertMany API signature, while the actual data remains strongly typed as *LogEntry.


145-153: LGTM! No-op implementations are appropriate and well-documented.

The comments clearly explain the design rationale - MongoDB writes are synchronous (no buffer to flush) and the client lifecycle is managed by the storage layer.

cmd/gomodel/main.go (4)

18-34: LGTM! Imports correctly structured.

Provider packages are properly imported with blank imports to trigger init() registration, as per coding guidelines. The pgxpool and mongo imports are necessary for type assertions in initAuditLogger.


229-255: LGTM! Graceful shutdown properly implemented.

The implementation correctly:

  • Uses a goroutine to handle shutdown signals asynchronously
  • Provides a 30-second timeout for graceful shutdown
  • Distinguishes between http.ErrServerClosed (graceful) and actual startup failures

This addresses the previous review concern about logging all errors as informational.


258-296: LGTM! Storage configuration properly combines settings.

The function correctly uses cfg.Logging.StorageType to select the backend while using shared connection settings from cfg.Storage. The defaults are sensible (SQLite as default type, reasonable paths).


297-336: LGTM! Log store creation with proper type handling.

The switch statement correctly:

  • Creates the appropriate LogStore based on storage type
  • Uses type assertions with proper error handling for PostgreSQL and MongoDB
  • Cleans up the storage connection on all error paths with store.Close()
  • Returns the storage handle for proper lifecycle management

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +93 to +97
_, err := collection.Indexes().CreateMany(ctx, indexes)
if err != nil {
// Log warning but don't fail - indexes may already exist
slog.Warn("failed to create some MongoDB indexes", "error", err)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider distinguishing recoverable vs critical index creation errors.

The current approach logs all index errors as warnings and continues. While this handles the common case of pre-existing indexes, it could mask critical errors (e.g., invalid TTL configuration, insufficient permissions).

Consider checking for specific error codes:

  • Code 85/86 (IndexOptionsConflict/IndexKeySpecsConflict) → warn and continue
  • Other errors → return error to fail fast
♻️ Optional: More precise error handling
 	_, err := collection.Indexes().CreateMany(ctx, indexes)
 	if err != nil {
-		// Log warning but don't fail - indexes may already exist
-		slog.Warn("failed to create some MongoDB indexes", "error", err)
+		// Check if it's an index conflict (indexes already exist)
+		var cmdErr mongo.CommandError
+		if errors.As(err, &cmdErr) && (cmdErr.Code == 85 || cmdErr.Code == 86) {
+			slog.Debug("indexes already exist", "error", err)
+		} else {
+			return nil, fmt.Errorf("failed to create indexes: %w", err)
+		}
 	}

Note: You would need to import errors and verify the exact error codes for your MongoDB driver version.

🤖 Prompt for AI Agents
In `@internal/auditlog/store_mongodb.go` around lines 93 - 97, Change the blanket
warning on index creation failures in the collection.Indexes().CreateMany call
to distinguish recoverable index conflicts from fatal errors: when err is a
mongo.CommandError (or unwraps to one) check the Code (85 or 86) and only log a
warning and continue for those codes, but for any other error return the error
so the caller fails fast; update the error handling around CreateMany in
store_mongodb.go (referencing collection.Indexes().CreateMany and the local err
variable) and import any needed packages (e.g., the mongo driver error type and
errors for unwrapping).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// otherwise use a regular descending index for query performance.
// MongoDB doesn't allow multiple indexes on the same field when one is TTL.
if retentionDays > 0 {
ttlSeconds := int32(retentionDays * 24 * 60 * 60)
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TTL calculation can overflow for large retention periods. With retentionDays * 24 * 60 * 60, values over ~24855 days (68 years) will overflow int32. While this is unlikely in practice, consider adding a validation check or using int64 for the calculation before converting to int32 to avoid unexpected behavior.

Suggested change
ttlSeconds := int32(retentionDays * 24 * 60 * 60)
// Prevent overflow when converting retentionDays to a TTL in seconds.
// MaxInt32 / (24 * 60 * 60) == 24855, so larger values would overflow.
const maxRetentionDaysForTTL = 24855
if retentionDays > maxRetentionDaysForTTL {
return nil, fmt.Errorf("retentionDays too large for TTL index; maximum supported is %d days", maxRetentionDaysForTTL)
}
ttlSeconds := int32(retentionDays) * 24 * 60 * 60

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +76
// Capture request body if enabled
if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
bodyBytes, err := io.ReadAll(req.Body)
if err == nil {
entry.Data.RequestBody = bodyBytes
// Restore the body for the handler
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request body is read without any size limit check beyond the Echo BodyLimit middleware. If LogBodies is enabled and a large request passes the Echo limit, io.ReadAll could consume significant memory. Consider adding an explicit size check here (e.g., max 1MB for audit logging) to prevent potential memory issues, especially since this data is held in memory until flushing occurs.

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +208
defer auditLogger.Close()
defer auditStore.Close()
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deferred calls to auditLogger.Close() and auditStore.Close() at lines 207-208 will only execute when the main function exits, but the server runs indefinitely until a signal is received. The graceful shutdown handler (lines 230-243) should close these resources to ensure audit logs are properly flushed before the application terminates. Consider moving these Close() calls into the shutdown handler.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +35
return func(c echo.Context) error {
// Skip if logging is disabled
if logger == nil || !logger.Config().Enabled {
return next(c)
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middleware should skip logging for health check, metrics, and favicon endpoints to avoid polluting audit logs with operational traffic. The SkipLoggingPaths function exists in stream_wrapper.go but is not being used. Consider adding a check at the beginning of the middleware function to skip these paths early.

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +245
// SkipLoggingPaths returns true if the path should skip logging
func SkipLoggingPaths(path string) bool {
skipPaths := []string{
"/health",
"/metrics",
"/favicon.ico",
}
for _, skip := range skipPaths {
if strings.HasPrefix(path, skip) {
return true
}
}
return false
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SkipLoggingPaths function is defined but never used. This creates dead code and suggests the middleware should be using it to filter out health checks, metrics, and favicon requests. Either remove this function if it's not needed, or use it in the middleware.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/providers/anthropic/anthropic.go (1)

328-349: Streaming chunk includes provider metadata.

The content_block_delta conversion now includes the "provider": "anthropic" field for consistency with non-streaming responses.

Per coding guidelines, consider defining a typed struct for these chunks instead of map[string]interface{} to catch errors at compile time. However, this is internal formatting code and the current approach is acceptable for JSON marshaling purposes.

internal/server/handlers.go (1)

63-69: Consider enriching token usage for non-streaming responses.

For non-streaming ChatCompletion, the response contains usage information (resp.Usage), but EnrichEntry is called with nil usage before the provider call. Consider enriching the entry with actual token counts after receiving the response.

Proposed enhancement for non-streaming usage tracking
 	// Non-streaming
 	resp, err := h.provider.ChatCompletion(c.Request().Context(), &req)
 	if err != nil {
 		return handleError(c, err)
 	}

+	// Enrich with actual token usage from response
+	if resp.Usage != nil {
+		auditlog.EnrichEntry(c, req.Model, h.provider.GetProviderType(req.Model), &auditlog.Usage{
+			PromptTokens:     resp.Usage.PromptTokens,
+			CompletionTokens: resp.Usage.CompletionTokens,
+			TotalTokens:      resp.Usage.TotalTokens,
+		})
+	}

 	return c.JSON(http.StatusOK, resp)

Verify if token usage enrichment is handled elsewhere (e.g., in the audit middleware when parsing the response body) or if this is an intentional gap for the initial implementation.

🤖 Fix all issues with AI agents
In `@config/config_test.go`:
- Around line 289-326: Add an edge case to
TestLoggingOnlyModelInteractionsFromEnv that verifies an empty string value for
LOGGING_ONLY_MODEL_INTERACTIONS falls back to the default true: add a test row
like {"empty string", "", true} to the tests slice and ensure the subtest uses
os.Setenv("LOGGING_ONLY_MODEL_INTERACTIONS", "") (not Unsetenv) and still defers
Unsetenv; the rest of the subtest should remain the same so Load() and
cfg.Logging.OnlyModelInteractions are asserted as before.

In `@config/config.go`:
- Around line 233-242: After populating LogConfig (constructed with
getEnvOrDefault/getEnvBool/getEnvIntOrDefault) add validation for
cfg.Logging.StorageType (or Logging.StorageType) against an explicit allowed set
(e.g., "sqlite", "postgresql", "mongodb"); if the value is not in the set return
an error from the configuration loader (fmt.Errorf) describing the invalid
LOGGING_STORAGE_TYPE and listing the allowed options so invalid values fail fast
during config load rather than later at storage initialization.

In `@internal/auditlog/stream_wrapper.go`:
- Around line 232-245: SkipLoggingPaths appears to be dead code (only referenced
by TestSkipLoggingPaths) while production middleware uses
IsModelInteractionPath; remove SkipLoggingPaths and its test if it’s legacy, or
if intended for production use, replace middleware usage of
IsModelInteractionPath with SkipLoggingPaths (or consolidate both into a single
canonical function name) and update all callers and tests to use the chosen
function (update audit middleware and auditlog_test.go accordingly). Ensure
references to SkipLoggingPaths, IsModelInteractionPath, and any tests are
updated/removed consistently to avoid orphaned code.

In `@internal/server/http.go`:
- Around line 62-95: The logging output differs between the two branches
(cfg.LogOnlyModelInteractions true uses middleware.RequestLoggerWithConfig +
custom LogValuesFunc calling slog.Info, while the else branch uses
middleware.RequestLogger()), causing inconsistent log formats; to fix, unify the
format by extracting/creating a single RequestLoggerWithConfig that includes
your LogValuesFunc (and a Skipper that returns false when you want all paths
logged) and use that in both branches (still keep the
auditlog.IsModelInteractionPath check as the Skipper when
LogOnlyModelInteractions is true), replacing the plain
middleware.RequestLogger() usage so both modes emit the same structured slog
fields.
♻️ Duplicate comments (13)
docker-compose.yaml (1)

50-62: MongoDB healthcheck and service configuration look correct.

The healthcheck using mongosh --eval "db.adminCommand('ping')" is appropriate for mongo:8, and the start_period of 10s allows MongoDB sufficient time to initialize before health checks begin.

Note: Previous review comments about MongoDB authentication and the comment scope ("production" vs development) remain valid and should be addressed.

config/config.go (1)

339-354: Missing environment variable overrides for BufferSize, FlushInterval, and RetentionDays.

The expandEnvVars function handles overrides for LOGGING_ENABLED, LOGGING_STORAGE_TYPE, LOGGING_LOG_BODIES, LOGGING_LOG_HEADERS, and LOGGING_ONLY_MODEL_INTERACTIONS, but LOGGING_BUFFER_SIZE, LOGGING_FLUSH_INTERVAL, and LOGGING_RETENTION_DAYS are missing. This means these values cannot be overridden via environment variables when a config file exists.

Proposed fix to add missing overrides
 	if onlyModel := os.Getenv("LOGGING_ONLY_MODEL_INTERACTIONS"); onlyModel != "" {
 		cfg.Logging.OnlyModelInteractions = strings.EqualFold(onlyModel, "true") || onlyModel == "1"
 	}
+	if bufferSize := os.Getenv("LOGGING_BUFFER_SIZE"); bufferSize != "" {
+		if size, err := strconv.Atoi(bufferSize); err == nil {
+			cfg.Logging.BufferSize = size
+		}
+	}
+	if flushInterval := os.Getenv("LOGGING_FLUSH_INTERVAL"); flushInterval != "" {
+		if interval, err := strconv.Atoi(flushInterval); err == nil {
+			cfg.Logging.FlushInterval = interval
+		}
+	}
+	if retentionDays := os.Getenv("LOGGING_RETENTION_DAYS"); retentionDays != "" {
+		if days, err := strconv.Atoi(retentionDays); err == nil {
+			cfg.Logging.RetentionDays = days
+		}
+	}
tests/e2e/auditlog_test.go (2)

464-466: Consider tightening the assertion tolerance.

The assertion allows up to 2 missing entries (numRequests-2), which may mask actual race conditions or dropped entries. With a buffer size of 1000 and only 20 concurrent requests, all entries should be logged successfully.


144-156: Server readiness check should fail the test if server doesn't start.

The readiness loop exits silently after 30 iterations without verifying the server actually started. If the server fails to start, the test will continue and fail with confusing errors.

🐛 Proposed fix
 	// Wait for server to be ready
 	client := &http.Client{Timeout: 2 * time.Second}
+	serverReady := false
 	for i := 0; i < 30; i++ {
 		resp, err := client.Get(serverURL + "/health")
 		if err == nil {
 			_ = resp.Body.Close()
 			if resp.StatusCode == http.StatusOK {
+				serverReady = true
 				break
 			}
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
+	require.True(t, serverReady, "server did not become ready within expected time")

 	return serverURL, srv, logger
internal/auditlog/auditlog.go (1)

95-118: Consider O(1) lookup for header redaction.

The current implementation has O(n×m) complexity where n is the number of headers and m is the number of redacted headers. For typical header counts this is fine, but a map-based lookup would be more efficient.

internal/auditlog/stream_wrapper.go (2)

101-138: Using map[string]interface{} for JSON parsing is pragmatic here.

Per coding guidelines, strongly-typed structs are preferred. However, since this parses responses from various providers with potentially different structures, the map approach is acceptable. The final Usage struct is strongly-typed.


149-185: Shallow copy of pointer fields shares underlying values.

Temperature and MaxTokens pointer fields are copied directly (lines 176-177), meaning both entries share the same underlying values. While typically safe since these shouldn't change after creation, consider deep copying for true isolation.

internal/auditlog/middleware.go (4)

164-168: Extract magic number to a named constant.

The 1MB limit (1024*1024) should be a named constant for clarity and maintainability.

♻️ Proposed refactor
+const maxResponseBodyCapture = 1024 * 1024 // 1MB limit

 func (r *responseBodyCapture) Write(b []byte) (int, error) {
 	// Write to the capture buffer (limit to 1MB to avoid memory issues)
-	if r.body.Len() < 1024*1024 {
+	if r.body.Len() < maxResponseBodyCapture {
 		r.body.Write(b)
 	}

228-249: Missing nil check for entry.Data in EnrichEntry.

EnrichEntry accesses entry.Data.PromptTokens without checking if entry.Data is nil, while EnrichEntryWithError and EnrichEntryWithStream include this check.

🐛 Proposed fix
 func EnrichEntry(c echo.Context, model, provider string, usage *Usage) {
 	entryVal := c.Get(string(LogEntryKey))
 	if entryVal == nil {
 		return
 	}

 	entry, ok := entryVal.(*LogEntry)
-	if !ok || entry == nil {
+	if !ok || entry == nil || entry.Data == nil {
 		return
 	}

 	entry.Model = model
 	entry.Provider = provider

 	if usage != nil {
 		entry.Data.PromptTokens = usage.PromptTokens

81-96: Unbounded request body read - potential memory exhaustion.

The request body is read fully into memory without a size limit. While the response body capture (line 166) has a 1MB cap, request bodies could be arbitrarily large if LogBodies is enabled.

🐛 Proposed fix - add size limit
 // Capture request body if enabled
-if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
-	bodyBytes, err := io.ReadAll(req.Body)
+const maxRequestBodyCapture int64 = 1024 * 1024 // 1MB limit
+if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
+	// Limit capture to prevent memory exhaustion
+	limitReader := io.LimitReader(req.Body, maxRequestBodyCapture)
+	bodyBytes, err := io.ReadAll(limitReader)
 	if err == nil {

Alternatively, skip capture for very large bodies:

-if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
+const maxRequestBodyCapture int64 = 1024 * 1024
+if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 && req.ContentLength <= maxRequestBodyCapture {

192-212: Duplicate functions: extractHeaders and extractEchoHeaders are identical.

Both functions have the exact same implementation. Consider consolidating into a single function.

♻️ Proposed refactor
-// extractHeaders extracts headers from http.Header, redacting sensitive ones
+// extractHeaders extracts headers from a header map (http.Header or Echo response headers),
+// taking only the first value of each key and redacting sensitive headers.
 func extractHeaders(headers map[string][]string) map[string]string {
 	result := make(map[string]string, len(headers))
 	for key, values := range headers {
 		if len(values) > 0 {
 			result[key] = values[0]
 		}
 	}
 	return RedactHeaders(result)
 }

-// extractEchoHeaders extracts headers from echo's header map
-func extractEchoHeaders(headers map[string][]string) map[string]string {
-	result := make(map[string]string, len(headers))
-	for key, values := range headers {
-		if len(values) > 0 {
-			result[key] = values[0]
-		}
-	}
-	return RedactHeaders(result)
-}

Then update line 122:

-entry.Data.ResponseHeaders = extractEchoHeaders(c.Response().Header())
+entry.Data.ResponseHeaders = extractHeaders(c.Response().Header())
cmd/gomodel/main.go (2)

350-356: Duplicate default application - defaults already set in NewLogger.

The defaults for BufferSize and FlushInterval are already applied in auditlog.NewLogger() (per relevant code snippet at internal/auditlog/logger.go:23-43). This creates duplicate logic.

♻️ Consider removing duplicate defaults
 	// Create logger configuration
 	logCfg := auditlog.Config{
 		Enabled:               cfg.Logging.Enabled,
 		LogBodies:             cfg.Logging.LogBodies,
 		LogHeaders:            cfg.Logging.LogHeaders,
 		BufferSize:            cfg.Logging.BufferSize,
 		FlushInterval:         time.Duration(cfg.Logging.FlushInterval) * time.Second,
 		RetentionDays:         cfg.Logging.RetentionDays,
 		OnlyModelInteractions: cfg.Logging.OnlyModelInteractions,
 	}

-	// Apply defaults
-	if logCfg.BufferSize <= 0 {
-		logCfg.BufferSize = 1000
-	}
-	if logCfg.FlushInterval <= 0 {
-		logCfg.FlushInterval = 5 * time.Second
-	}
+	// Note: NewLogger() applies defaults for BufferSize and FlushInterval if <= 0

 	return auditlog.NewLogger(logStore, logCfg), store, nil

207-208: Verify cleanup order: logger should close before store.

The deferred calls execute in LIFO order, so auditStore.Close() (line 208) will execute before auditLogger.Close() (line 207). This is correct - the logger should flush before the store closes. However, the current order means the store closes first.

🐛 Proposed fix - swap order for correct LIFO execution
-		defer auditLogger.Close()
-		defer auditStore.Close()
+		defer auditStore.Close()  // Executes second (after logger flushes)
+		defer auditLogger.Close() // Executes first (flushes buffered entries)
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4794932 and 8775c87.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (26)
  • .claude/settings.local.json
  • cmd/gomodel/main.go
  • config/config.go
  • config/config_test.go
  • docker-compose.yaml
  • go.mod
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/stream_wrapper.go
  • internal/core/interfaces.go
  • internal/core/responses.go
  • internal/core/types.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/gemini/gemini.go
  • internal/providers/groq/groq.go
  • internal/providers/groq/groq_test.go
  • internal/providers/openai/openai.go
  • internal/providers/registry.go
  • internal/providers/responses_converter.go
  • internal/providers/router.go
  • internal/providers/xai/xai.go
  • internal/server/handlers.go
  • internal/server/handlers_test.go
  • internal/server/http.go
  • tests/e2e/auditlog_test.go
🧰 Additional context used
📓 Path-based instructions (11)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/providers/registry.go
  • internal/core/responses.go
  • internal/providers/openai/openai.go
  • tests/e2e/auditlog_test.go
  • internal/providers/xai/xai.go
  • config/config_test.go
  • internal/providers/responses_converter.go
  • internal/core/interfaces.go
  • internal/providers/router.go
  • cmd/gomodel/main.go
  • internal/providers/groq/groq.go
  • internal/server/http.go
  • internal/providers/anthropic/anthropic.go
  • internal/core/types.go
  • internal/providers/gemini/gemini.go
  • internal/server/handlers_test.go
  • internal/server/handlers.go
  • internal/providers/groq/groq_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/stream_wrapper.go
  • config/config.go
internal/providers/{router,registry}.go

📄 CodeRabbit inference engine (CLAUDE.md)

The ModelRegistry must be initialized before routing requests; check ModelCount() > 0 or use router after InitializeAsync() completes to avoid ErrRegistryNotInitialized errors

Files:

  • internal/providers/registry.go
  • internal/providers/router.go
internal/providers/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Streaming responses must return io.ReadCloser and callers are responsible for closing the stream

Files:

  • internal/providers/registry.go
  • internal/providers/openai/openai.go
  • internal/providers/xai/xai.go
  • internal/providers/responses_converter.go
  • internal/providers/router.go
  • internal/providers/groq/groq.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/gemini/gemini.go
  • internal/providers/groq/groq_test.go
internal/providers/registry.go

📄 CodeRabbit inference engine (CLAUDE.md)

internal/providers/registry.go: Use RWMutex for thread-safe concurrent access to the ModelRegistry
Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Files:

  • internal/providers/registry.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • tests/e2e/auditlog_test.go
  • config/config_test.go
  • internal/server/handlers_test.go
  • internal/providers/groq/groq_test.go
  • internal/auditlog/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/auditlog_test.go
internal/providers/router.go

📄 CodeRabbit inference engine (CLAUDE.md)

When multiple providers support the same model, the first registered provider wins the routing decision

Files:

  • internal/providers/router.go
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
internal/server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use Echo (v4) framework for HTTP server setup and middleware, not Chi or other frameworks

Files:

  • internal/server/http.go
  • internal/server/handlers_test.go
  • internal/server/handlers.go
go.mod

📄 CodeRabbit inference engine (CLAUDE.md)

Go version must be 1.24.0 or later

Files:

  • go.mod
config/config.go

📄 CodeRabbit inference engine (CLAUDE.md)

config/config.go: Use Viper for loading configuration from environment variables and .env files in the config package
At least one provider API key is required in the environment configuration to start the server

Files:

  • config/config.go
🧠 Learnings (17)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • internal/providers/registry.go
  • cmd/gomodel/main.go
  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Applied to files:

  • internal/providers/registry.go
  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/{router,registry}.go : The ModelRegistry must be initialized before routing requests; check `ModelCount() > 0` or use router after `InitializeAsync()` completes to avoid `ErrRegistryNotInitialized` errors

Applied to files:

  • internal/providers/registry.go
  • internal/providers/router.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • internal/providers/registry.go
  • internal/providers/openai/openai.go
  • internal/providers/xai/xai.go
  • internal/providers/responses_converter.go
  • internal/core/interfaces.go
  • internal/providers/router.go
  • cmd/gomodel/main.go
  • internal/providers/gemini/gemini.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/router.go : When multiple providers support the same model, the first registered provider wins the routing decision

Applied to files:

  • internal/providers/registry.go
  • internal/core/interfaces.go
  • internal/providers/router.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • internal/providers/registry.go
  • internal/core/responses.go
  • internal/providers/openai/openai.go
  • tests/e2e/auditlog_test.go
  • internal/providers/xai/xai.go
  • internal/providers/responses_converter.go
  • internal/core/interfaces.go
  • internal/providers/router.go
  • internal/providers/groq/groq.go
  • internal/providers/anthropic/anthropic.go
  • internal/core/types.go
  • internal/providers/gemini/gemini.go
  • internal/server/handlers_test.go
  • internal/server/handlers.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Each provider package must include an `init()` function that registers itself with the factory using `providers.RegisterFactory("provider-name", NewProvider)`

Applied to files:

  • internal/providers/registry.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : The responses converter in `internal/providers/responses_converter.go` must handle the OpenAI→Responses stream conversion for all providers

Applied to files:

  • internal/core/responses.go
  • internal/providers/openai/openai.go
  • internal/providers/xai/xai.go
  • internal/providers/responses_converter.go
  • internal/providers/groq/groq.go
  • internal/providers/anthropic/anthropic.go
  • internal/core/types.go
  • internal/providers/gemini/gemini.go
  • internal/providers/groq/groq_test.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/core/responses.go
  • internal/providers/responses_converter.go
  • internal/providers/groq/groq.go
  • internal/providers/gemini/gemini.go
  • internal/providers/groq/groq_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/auditlog_test.go
  • go.mod
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • tests/e2e/auditlog_test.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • config/config_test.go
  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/server/http.go
  • internal/server/handlers.go
  • internal/auditlog/middleware.go
  • go.mod
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/cache/**/*.go : Cache structure must use `ModelCache` struct with Version, UpdatedAt, and Models (map[string]CachedModel) fields

Applied to files:

  • internal/server/handlers.go
  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to go.mod : Go version must be 1.24.0 or later

Applied to files:

  • go.mod
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : Use `Viper` for loading configuration from environment variables and .env files in the config package

Applied to files:

  • config/config.go
🧬 Code graph analysis (17)
internal/core/responses.go (6)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/anthropic/anthropic.go (1)
  • Provider (34-37)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/openai/openai.go (1)
  • Provider (24-27)
internal/providers/xai/xai.go (1)
  • Provider (24-27)
internal/providers/openai/openai.go (6)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/anthropic/anthropic.go (1)
  • Provider (34-37)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/xai/xai.go (1)
  • Provider (24-27)
internal/core/types.go (1)
  • Model (56-61)
tests/e2e/auditlog_test.go (4)
internal/auditlog/auditlog.go (2)
  • LogEntry (28-45)
  • Config (121-143)
internal/server/http.go (3)
  • Config (25-32)
  • Server (19-22)
  • New (35-130)
internal/auditlog/logger.go (2)
  • Logger (13-20)
  • NewLogger (24-44)
internal/core/types.go (3)
  • ChatRequest (4-10)
  • Model (56-61)
  • Message (25-28)
internal/providers/xai/xai.go (6)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/anthropic/anthropic.go (1)
  • Provider (34-37)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/openai/openai.go (1)
  • Provider (24-27)
internal/core/types.go (1)
  • Model (56-61)
config/config_test.go (1)
config/config.go (1)
  • Load (156-292)
cmd/gomodel/main.go (8)
internal/auditlog/logger.go (2)
  • LoggerInterface (169-173)
  • NewLogger (24-44)
internal/storage/storage.go (9)
  • Storage (58-78)
  • Config (20-32)
  • New (82-93)
  • SQLiteConfig (35-38)
  • PostgreSQLConfig (41-46)
  • MongoDBConfig (49-54)
  • TypeSQLite (14-14)
  • TypePostgreSQL (15-15)
  • TypeMongoDB (16-16)
config/config.go (1)
  • Config (26-33)
internal/auditlog/auditlog.go (2)
  • Config (121-143)
  • LogStore (13-24)
internal/server/http.go (3)
  • Config (25-32)
  • Server (19-22)
  • New (35-130)
internal/auditlog/store_sqlite.go (1)
  • NewSQLiteStore (23-69)
internal/auditlog/store_postgresql.go (1)
  • NewPostgreSQLStore (23-72)
internal/auditlog/store_mongodb.go (1)
  • NewMongoDBStore (55-103)
internal/providers/groq/groq.go (4)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/core/types.go (1)
  • Model (56-61)
internal/providers/responses_converter.go (1)
  • NewOpenAIResponsesStreamConverter (31-40)
internal/server/http.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/stream_wrapper.go (1)
  • IsModelInteractionPath (248-260)
internal/auditlog/auditlog.go (1)
  • Config (121-143)
internal/auditlog/middleware.go (1)
  • Middleware (34-154)
internal/providers/anthropic/anthropic.go (6)
internal/core/types.go (1)
  • Model (56-61)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/openai/openai.go (1)
  • Provider (24-27)
internal/providers/xai/xai.go (1)
  • Provider (24-27)
internal/providers/gemini/gemini.go (3)
internal/providers/anthropic/anthropic.go (1)
  • Provider (34-37)
internal/core/types.go (1)
  • Model (56-61)
internal/providers/responses_converter.go (1)
  • NewOpenAIResponsesStreamConverter (31-40)
internal/server/handlers.go (2)
internal/auditlog/middleware.go (1)
  • EnrichEntry (230-249)
internal/core/types.go (1)
  • Model (56-61)
internal/providers/groq/groq_test.go (1)
internal/providers/responses_converter.go (1)
  • NewOpenAIResponsesStreamConverter (31-40)
internal/auditlog/auditlog.go (1)
internal/server/http.go (1)
  • Config (25-32)
internal/auditlog/auditlog_test.go (2)
internal/auditlog/auditlog.go (4)
  • RedactHeaders (97-118)
  • LogEntry (28-45)
  • LogData (49-80)
  • Config (121-143)
internal/auditlog/stream_wrapper.go (5)
  • SkipLoggingPaths (233-245)
  • IsModelInteractionPath (248-260)
  • NewStreamLogWrapper (23-29)
  • WrapStreamForLogging (142-147)
  • CreateStreamEntry (151-185)
internal/auditlog/middleware.go (3)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/stream_wrapper.go (2)
  • IsModelInteractionPath (248-260)
  • IsEntryMarkedAsStreaming (223-230)
internal/core/types.go (2)
  • Usage (49-53)
  • Model (56-61)
internal/auditlog/stream_wrapper.go (5)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/auditlog.go (3)
  • LogEntry (28-45)
  • Config (121-143)
  • LogData (49-80)
internal/auditlog/middleware.go (1)
  • Usage (341-345)
internal/core/types.go (2)
  • Usage (49-53)
  • Model (56-61)
internal/server/http.go (1)
  • Config (25-32)
config/config.go (1)
internal/storage/storage.go (1)
  • Storage (58-78)
🪛 ast-grep (0.40.5)
tests/e2e/auditlog_test.go

[warning] 111-111: "Detected a network listener listening on 0.0.0.0 or an empty string.
This could unexpectedly expose the server publicly as it binds to all
available interfaces. Instead, specify another IP address that is not
0.0.0.0 nor the empty string."
Context: net.Listen("tcp", ":0")
Note: [CWE-200] Exposure of Sensitive Information to an Unauthorized Actor [REFERENCES]
- https://owasp.org/Top10/A01_2021-Broken_Access_Control

(avoid-bind-to-all-interfaces-go)

🔇 Additional comments (65)
.claude/settings.local.json (1)

8-13: LGTM! Appropriate development permissions.

The new permissions are reasonable for development workflows—build, lint, test, and documentation commands are all non-destructive operations.

One consideration: .local.json files typically contain user-specific settings and are often excluded from version control. If this is intentional for team consistency, consider documenting this in the repository README or contributing guide.

internal/core/types.go (1)

31-39: LGTM! Provider metadata fields added consistently.

The Provider and Created fields extend ChatResponse with necessary metadata for audit logging. This aligns with similar changes across other providers and follows the coding guideline requiring strongly-typed structs.

internal/core/responses.go (1)

44-55: LGTM! Provider field consistently added to ResponsesResponse.

This mirrors the Provider field addition in ChatResponse, ensuring both chat completions and responses API payloads carry provider metadata for audit logging purposes.

internal/providers/openai/openai.go (2)

70-74: LGTM! Provider metadata enrichment in ChatCompletion.

Setting resp.Provider = "openai" and defaulting resp.Model from the request when empty ensures consistent metadata propagation for audit logging.


110-114: LGTM! Consistent provider metadata enrichment in Responses.

The same pattern applied to the Responses API ensures uniform provider metadata across both endpoints.

internal/providers/registry.go (1)

344-356: LGTM! Thread-safe provider type lookup.

The implementation correctly uses RLock for read-only access per coding guidelines. The method follows the established pattern of other registry lookups (GetProvider, GetModel) and gracefully returns an empty string for unknown models.

internal/providers/responses_converter.go (5)

17-27: LGTM! Provider field added to stream converter.

The provider field enables consistent provider metadata in all streaming events, aligning with the non-streaming response types.


54-78: LGTM! Provider injected into response.created event.

The provider metadata is correctly included in the initial streaming event.


103-127: LGTM! Provider injected into response.done event.

Provider metadata is consistently included when the [DONE] marker is received.


158-194: LGTM! Provider injected into final response.done on EOF.

The EOF handling path also correctly includes provider metadata, ensuring consistency regardless of how the stream terminates.


29-40: All callers of the NewOpenAIResponsesStreamConverter constructor have been updated with the new provider parameter. Gemini (line 315) and Groq (line 232) both pass the provider string correctly, and test code is also in sync.

internal/providers/groq/groq.go (3)

73-77: LGTM! Provider metadata and model fallback logic.

The addition of Provider field and the defensive fallback for empty Model aligns with the provider tagging pattern used across other providers (anthropic, gemini, xai).


180-180: Provider propagation is consistent.

Correctly propagates the provider field from ChatResponse to ResponsesResponse, maintaining consistency in the response chain.


232-232: Stream converter call updated correctly.

The third parameter "groq" matches the updated NewOpenAIResponsesStreamConverter signature in internal/providers/responses_converter.go (lines 30-39).

go.mod (2)

3-3: Go version requirement satisfied.

Go 1.24.0 meets the required minimum version per coding guidelines.


6-16: Dependencies appropriately support audit logging and storage backends.

The added dependencies are correctly utilized:

  • brotli for decompression of brotli-encoded response bodies in audit logging middleware
  • pgx/v5 for PostgreSQL storage backend via pgxpool
  • mongo-driver/v2 for MongoDB backend in audit logging and storage
  • sqlite for SQLite backend (pure Go implementation)

Go version 1.24.0 meets the requirement (≥1.24.0).

internal/providers/groq/groq_test.go (3)

889-891: Test correctly updated for new converter signature.

The test passes "groq" as the provider parameter, matching the updated NewOpenAIResponsesStreamConverter signature. Test coverage for stream conversion is comprehensive.


922-924: Close behavior test updated correctly.

Properly tests Close() method behavior with the new three-argument constructor.


949-951: Empty delta test updated correctly.

This test validates that empty deltas are filtered out, ensuring only meaningful content is emitted.

internal/providers/anthropic/anthropic.go (5)

168-172: Provider metadata and timestamp added correctly.

The additions align with the provider tagging pattern across all providers. Using time.Now().Unix() for Created is appropriate since Anthropic's API doesn't return a creation timestamp.


357-377: Message delta streaming chunk updated consistently.

The message_delta handler mirrors the content_block_delta pattern with provider metadata.


513-513: ResponsesResponse includes provider field.

Correctly sets Provider: "anthropic" in the Responses API conversion.


614-632: Response.done event includes provider metadata.

The final streaming event correctly propagates the provider field for audit logging and client consumption.


690-706: Response.created event includes provider metadata.

The initial streaming event correctly includes provider information, enabling downstream consumers to identify the source.

internal/auditlog/auditlog_test.go (6)

217-252: Thread-safe mockStore implementation addresses race condition.

The mockStore now includes proper mutex synchronization with getEntries() and isClosed() helper methods for safe concurrent access. This correctly addresses the previous review feedback about race conditions.


265-280: Logger test uses proper entry ID format.

The entry ID generation now uses fmt.Sprintf("entry-%d", i) for clearer test output, addressing previous review feedback.

The time.Sleep(200 * time.Millisecond) approach is acceptable here since the flush interval is 100ms, providing adequate margin. For more robust tests, consider the WaitForEntries pattern suggested in previous reviews, but this is not blocking.


461-522: Stream wrapper test validates usage extraction.

Comprehensive test that verifies:

  • SSE stream content is read correctly
  • Usage data (prompt/completion/total tokens) is extracted on close
  • Entry is logged asynchronously

The test correctly uses time.Sleep(200 * time.Millisecond) after close to allow async write to complete.


601-659: API key hashing tests are thorough.

Tests cover edge cases (empty, Bearer-only, valid tokens) and verify:

  • Consistent hashing for same input
  • Different hashes for different inputs
  • 8-character hash length

661-684: Compression helper functions handle errors appropriately.

The helper functions compressGzip, compressDeflate, and compressBrotli use blank identifiers for error handling, which is acceptable in test helper code where the input is controlled.


686-800: Decompression tests provide comprehensive coverage.

Tests cover:

  • All supported encodings (gzip, deflate, brotli)
  • Edge cases (empty, identity, unknown encoding)
  • Case insensitivity (GZIP)
  • Multiple encodings header
  • Invalid compressed data handling
  • Empty/nil input handling

This aligns well with the brotli dependency added in go.mod.

docker-compose.yaml (1)

12-27: LGTM on audit logging configuration.

The environment variables for audit logging are well-structured with sensible defaults (buffer size 1000, flush interval 5s, retention 30 days). The configuration aligns with the LogConfig struct in config/config.go.

config/config.go (2)

35-72: Well-documented LogConfig struct with clear defaults.

The struct documentation is comprehensive, with clear warnings about sensitive data (PII, API keys) when LogBodies is enabled. The field defaults are sensible.


411-442: Clean helper functions for environment variable handling.

The helper functions (getEnvOrDefault, getEnvIntOrDefault, getEnvBool, getEnvBoolOrDefault) are well-implemented and provide consistent behavior for environment variable parsing across the configuration.

config/config_test.go (1)

270-287: LGTM: Default value test is correct.

The test properly validates that OnlyModelInteractions defaults to true when the environment variable is not set, which matches the getEnvBoolOrDefault("LOGGING_ONLY_MODEL_INTERACTIONS", true) implementation.

internal/providers/router.go (1)

116-121: LGTM: Clean delegation to registry.

The method correctly delegates to the registry and returns an empty string for unknown models. Unlike operational methods (ChatCompletion, Responses), this informational lookup appropriately avoids checkReady() since returning an empty string is a safe fallback for audit logging enrichment.

internal/server/handlers.go (2)

38-39: Audit log enrichment placed correctly before provider delegation.

The enrichment call is appropriately positioned after model validation and before the streaming/non-streaming branch, ensuring the audit log captures model and provider information regardless of the response path.


102-103: Consistent enrichment pattern in Responses handler.

The enrichment follows the same pattern as ChatCompletion, maintaining consistency across handlers.

internal/providers/xai/xai.go (2)

70-74: LGTM!

The provider metadata enrichment correctly sets resp.Provider = "xai" and provides a sensible fallback for resp.Model when the upstream API doesn't return it. This aligns with the standardized provider tagging pattern used across other providers (openai, groq, gemini, anthropic).


110-114: LGTM!

Consistent provider enrichment for the Responses API path, mirroring the ChatCompletion implementation.

internal/server/handlers_test.go (1)

35-40: LGTM!

The mock correctly implements the new GetProviderType interface method, returning "mock" for supported models and empty string otherwise. This enables tests to exercise audit log enrichment code paths.

internal/server/http.go (2)

26-31: LGTM - Config struct extension for audit logging.

The new fields follow Go naming conventions and integrate cleanly with the existing configuration pattern. Using auditlog.LoggerInterface allows for flexible implementations (SQLite, PostgreSQL, MongoDB as mentioned in PR objectives).


105-108: Verify intended middleware ordering: audit logging before authentication.

The audit middleware is placed after BodyLimit but before AuthMiddleware. This means:

  • Unauthenticated requests will be logged before being rejected
  • Request bodies will be available (after BodyLimit processing)

If this is intentional (to audit failed auth attempts), the ordering is correct. If audit logs should only contain authenticated requests, move the audit middleware after auth.

internal/providers/gemini/gemini.go (3)

84-88: LGTM!

Provider enrichment follows the established pattern. Setting resp.Provider = "gemini" and defaulting resp.Model ensures audit logs have complete metadata.


264-264: LGTM - Provider field propagation.

Correctly propagates the Provider field from the chat response to the Responses API response, ensuring consistent metadata across both API formats.


315-315: LGTM!

The stream converter call correctly passes the "gemini" provider identifier, matching the updated NewOpenAIResponsesStreamConverter(reader, model, provider) signature shown in the relevant code snippets.

internal/core/interfaces.go (1)

35-38: LGTM - Interface extension for provider type lookup.

Clean addition to enable audit log enrichment. The empty string return for unknown models is a reasonable sentinel value. The Router type correctly implements this method by delegating to the ModelRegistry, which provides a thread-safe lookup with proper lock handling.

tests/e2e/auditlog_test.go (6)

1-3: LGTM! Build tag compliance.

The //go:build e2e tag correctly marks this as an e2e test file per coding guidelines.


26-105: LGTM! Thread-safe mock store implementation.

The mockLogStore correctly uses sync.Mutex for thread-safe access and provides proper defensive copies in GetEntries() and GetAPIEntries(). The wait helpers with polling are appropriate for async testing.


111-115: Static analysis false positive - test-only binding is intentional.

The net.Listen("tcp", ":0") binding to all interfaces is flagged by static analysis, but this is appropriate for test code that needs an ephemeral port. The listener is immediately closed after obtaining the port number, and the actual server binds to localhost via the URL construction at line 137.


160-376: LGTM! Comprehensive audit log middleware test coverage.

The test cases properly use core.ChatRequest strongly-typed structs per coding guidelines, cover basic metadata capture, body/header logging, redaction, disabled logging, and API key hashing. Cleanup with deferred shutdown is correctly implemented.


242-246: Type assertion from interface{} is acceptable here for test verification.

The assertion to map[string]interface{} at line 243 is necessary to verify the stored request body content. Since LogData.RequestBody is typed as interface{} (for MongoDB BSON compatibility as noted in the relevant code snippets), this test-side assertion is appropriate.


611-769: LGTM! OnlyModelInteractions filtering tests.

Good coverage of the OnlyModelInteractions configuration option, including edge cases for health endpoint filtering, mixed request scenarios, and verifying that model endpoints are still logged when the option is enabled.

internal/auditlog/auditlog.go (3)

11-24: LGTM! Well-designed LogStore interface.

The interface is minimal and properly documents concurrency safety requirements. The three-method design (WriteBatch, Flush, Close) provides a clean contract for storage backends.


26-80: LGTM! Well-structured log entry types with appropriate tags.

LogEntry and LogData structs have proper JSON and BSON tags for dual storage compatibility. The use of interface{} for RequestBody/ResponseBody (lines 78-79) is intentionally documented for MongoDB BSON serialization, which is an acceptable trade-off for queryability.


120-156: LGTM! Sensible configuration defaults.

The Config struct is well-documented and DefaultConfig() provides secure defaults (logging disabled, bodies/headers not logged, 30-day retention). The OnlyModelInteractions default of true appropriately limits logging to relevant AI endpoints.

internal/auditlog/middleware.go (3)

34-47: LGTM! Proper middleware guards.

The middleware correctly checks for disabled logging and OnlyModelInteractions configuration before processing, avoiding unnecessary work for skipped paths.


156-190: LGTM! Proper http.Flusher and http.Hijacker delegation.

The responseBodyCapture wrapper correctly implements Flush() for SSE streaming and Hijack() for WebSocket upgrades by delegating to the underlying ResponseWriter. The 1MB buffer limit prevents memory exhaustion on response capture.


294-338: LGTM! Robust decompression with bomb protection.

The decompressBody function correctly handles multiple encodings (gzip, deflate, brotli) and includes a 2MB size limit to protect against compression bombs.

cmd/gomodel/main.go (4)

29-34: LGTM! Provider packages correctly imported with blank imports.

The provider packages are imported with blank imports (_) to trigger their init() registration, as required by coding guidelines.


198-217: LGTM! Proper audit logger initialization with cleanup.

The audit logging is correctly initialized with fallback to NoopLogger when disabled. Both auditLogger.Close() and auditStore.Close() are properly deferred for cleanup.


230-244: LGTM! Proper graceful shutdown with signal handling.

The shutdown handler correctly listens for SIGINT/SIGTERM, uses a 30-second timeout context, and logs any shutdown errors.


249-256: LGTM! Proper distinction between graceful shutdown and startup errors.

The error handling correctly differentiates http.ErrServerClosed (graceful shutdown) from actual startup errors, exiting with status 1 for the latter.

internal/auditlog/stream_wrapper.go (3)

10-46: LGTM! Efficient stream wrapping with bounded buffer.

The StreamLogWrapper correctly buffers the last 8KB of stream data to capture the final usage event, preventing unbounded memory growth for long-running streams.


48-69: LGTM! Proper Close implementation with idempotency.

The Close() method is idempotent (checks w.closed), correctly parses usage data before logging, and delegates to the underlying stream's Close.


247-260: LGTM! Clear model interaction path detection.

The IsModelInteractionPath function correctly identifies AI model endpoints using prefix matching. The paths align with the documented behavior in Config.OnlyModelInteractions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +289 to +326
func TestLoggingOnlyModelInteractionsFromEnv(t *testing.T) {
tests := []struct {
name string
envValue string
expected bool
}{
{"true lowercase", "true", true},
{"TRUE uppercase", "TRUE", true},
{"True mixed", "True", true},
{"false lowercase", "false", false},
{"FALSE uppercase", "FALSE", false},
{"False mixed", "False", false},
{"1 numeric", "1", true},
{"0 numeric", "0", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset viper state before each subtest
viper.Reset()

// Clear and set environment variable
_ = os.Unsetenv("OPENAI_API_KEY")
_ = os.Setenv("LOGGING_ONLY_MODEL_INTERACTIONS", tt.envValue)
defer func() { _ = os.Unsetenv("LOGGING_ONLY_MODEL_INTERACTIONS") }()

cfg, err := Load()
if err != nil {
t.Fatalf("Load() failed: %v", err)
}

if cfg.Logging.OnlyModelInteractions != tt.expected {
t.Errorf("expected OnlyModelInteractions=%v for env value %q, got %v",
tt.expected, tt.envValue, cfg.Logging.OnlyModelInteractions)
}
})
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Good table-driven test coverage for environment variable parsing.

The test covers a comprehensive range of inputs including case variations and numeric values. Consider adding an edge case for empty string to verify it falls back to the default value true.

Optional: Add empty string edge case
 	tests := []struct {
 		name     string
 		envValue string
 		expected bool
 	}{
+		{"empty string defaults to true", "", true},
 		{"true lowercase", "true", true},
 		// ... rest of tests
 	}

Note: This would require modifying the test to handle the empty string case differently since os.Setenv with empty string is different from os.Unsetenv.

🤖 Prompt for AI Agents
In `@config/config_test.go` around lines 289 - 326, Add an edge case to
TestLoggingOnlyModelInteractionsFromEnv that verifies an empty string value for
LOGGING_ONLY_MODEL_INTERACTIONS falls back to the default true: add a test row
like {"empty string", "", true} to the tests slice and ensure the subtest uses
os.Setenv("LOGGING_ONLY_MODEL_INTERACTIONS", "") (not Unsetenv) and still defers
Unsetenv; the rest of the subtest should remain the same so Load() and
cfg.Logging.OnlyModelInteractions are asserted as before.

Comment on lines +233 to +242
Logging: LogConfig{
Enabled: getEnvBool("LOGGING_ENABLED"),
StorageType: getEnvOrDefault("LOGGING_STORAGE_TYPE", "sqlite"),
LogBodies: getEnvBool("LOGGING_LOG_BODIES"),
LogHeaders: getEnvBool("LOGGING_LOG_HEADERS"),
BufferSize: getEnvIntOrDefault("LOGGING_BUFFER_SIZE", 1000),
FlushInterval: getEnvIntOrDefault("LOGGING_FLUSH_INTERVAL", 5),
RetentionDays: getEnvIntOrDefault("LOGGING_RETENTION_DAYS", 30),
OnlyModelInteractions: getEnvBoolOrDefault("LOGGING_ONLY_MODEL_INTERACTIONS", true),
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating storage type during configuration loading.

Invalid storage types (e.g., "redis", "mysql") are silently accepted during config loading and will only fail at runtime when initializing storage. Adding validation here would provide earlier feedback.

Proposed validation
// Add after loading LogConfig
validStorageTypes := map[string]bool{"sqlite": true, "postgresql": true, "mongodb": true}
if !validStorageTypes[cfg.Logging.StorageType] {
    return nil, fmt.Errorf("invalid LOGGING_STORAGE_TYPE %q: must be one of sqlite, postgresql, mongodb", cfg.Logging.StorageType)
}
🤖 Prompt for AI Agents
In `@config/config.go` around lines 233 - 242, After populating LogConfig
(constructed with getEnvOrDefault/getEnvBool/getEnvIntOrDefault) add validation
for cfg.Logging.StorageType (or Logging.StorageType) against an explicit allowed
set (e.g., "sqlite", "postgresql", "mongodb"); if the value is not in the set
return an error from the configuration loader (fmt.Errorf) describing the
invalid LOGGING_STORAGE_TYPE and listing the allowed options so invalid values
fail fast during config load rather than later at storage initialization.

Comment on lines +62 to +95
// Request logger with optional filtering for model-only interactions
if cfg != nil && cfg.LogOnlyModelInteractions {
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Skipper: func(c echo.Context) bool {
return !auditlog.IsModelInteractionPath(c.Request().URL.Path)
},
LogStatus: true,
LogURI: true,
LogError: true,
LogMethod: true,
LogLatency: true,
LogProtocol: true,
LogRemoteIP: true,
LogHost: true,
LogURIPath: true,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
slog.Info("REQUEST",
"method", v.Method,
"uri", v.URI,
"status", v.Status,
"latency", v.Latency.String(),
"host", v.Host,
"bytes_in", c.Request().ContentLength,
"bytes_out", c.Response().Size,
"user_agent", c.Request().UserAgent(),
"remote_ip", v.RemoteIP,
"request_id", c.Request().Header.Get("X-Request-ID"),
)
return nil
},
}))
} else {
e.Use(middleware.RequestLogger())
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider output format consistency between logging modes.

When LogOnlyModelInteractions is true, the custom LogValuesFunc produces structured slog.Info output with specific fields. When false, middleware.RequestLogger() is used which has different default formatting.

This asymmetry may cause confusion in log aggregation systems. Consider either:

  1. Using a custom LogValuesFunc in both cases for consistent output format
  2. Documenting the different output formats based on this flag
♻️ Unified logging format suggestion
-	} else {
-		e.Use(middleware.RequestLogger())
-	}
+	} else {
+		e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+			LogStatus:   true,
+			LogURI:      true,
+			LogError:    true,
+			LogMethod:   true,
+			LogLatency:  true,
+			LogProtocol: true,
+			LogRemoteIP: true,
+			LogHost:     true,
+			LogURIPath:  true,
+			LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+				slog.Info("REQUEST",
+					"method", v.Method,
+					"uri", v.URI,
+					"status", v.Status,
+					"latency", v.Latency.String(),
+					"host", v.Host,
+					"bytes_in", c.Request().ContentLength,
+					"bytes_out", c.Response().Size,
+					"user_agent", c.Request().UserAgent(),
+					"remote_ip", v.RemoteIP,
+					"request_id", c.Request().Header.Get("X-Request-ID"),
+				)
+				return nil
+			},
+		}))
+	}
🤖 Prompt for AI Agents
In `@internal/server/http.go` around lines 62 - 95, The logging output differs
between the two branches (cfg.LogOnlyModelInteractions true uses
middleware.RequestLoggerWithConfig + custom LogValuesFunc calling slog.Info,
while the else branch uses middleware.RequestLogger()), causing inconsistent log
formats; to fix, unify the format by extracting/creating a single
RequestLoggerWithConfig that includes your LogValuesFunc (and a Skipper that
returns false when you want all paths logged) and use that in both branches
(still keep the auditlog.IsModelInteractionPath check as the Skipper when
LogOnlyModelInteractions is true), replacing the plain
middleware.RequestLogger() usage so both modes emit the same structured slog
fields.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/server/handlers.go (1)

176-190: Error handler uses map[string]interface{} for fallback response.

Per coding guidelines, strongly-typed structs are preferred. However, this is a fallback error response that's only triggered for unexpected errors, so the pragmatic approach is acceptable here.

🤖 Fix all issues with AI agents
In `@internal/auditlog/stream_wrapper.go`:
- Around line 245-258: IsModelInteractionPath currently hardcodes model endpoint
paths; change it to read the paths from the audit log configuration so they are
configurable. Update the function (or add a wrapper) to accept or fetch the
Config (e.g., AuditLog Config struct / field that exposes ModelPaths or
ModelInteractionPaths) and iterate that slice instead of the inline modelPaths,
provide a sensible default fallback (the current three paths) when the config
slice is empty, and ensure any callers of IsModelInteractionPath are adjusted to
pass or make the config available (or use a new IsModelInteractionPathWithConfig
helper) so behavior is configurable at runtime.
- Around line 39-54: In StreamLogWrapper.Read, check and handle the error
returned by w.buffer.Write when buffering p[:n] (even though bytes.Buffer.Write
currently never fails); capture the write error (e.g., errBuf :=
w.buffer.Write(...)) and if non-nil either return it (or wrap/record it via the
component's error handling/logging) so partial write failures aren’t silently
ignored, keeping the existing truncation logic for buffer size intact.
♻️ Duplicate comments (2)
tests/e2e/auditlog_test.go (2)

559-561: Tolerance allows dropped log entries to go undetected.

With a buffer size of 1000 and only 20 concurrent requests, all entries should be logged successfully. The current assertion tolerates up to 2 missing entries, which could mask race conditions or dropped entries in the logging pipeline.

🔧 Proposed fix
 		// Wait for all log entries
 		entries := store.WaitForAPIEntries(numRequests, 5*time.Second)
-		assert.GreaterOrEqual(t, len(entries), numRequests-2, "Expected most requests to be logged")
+		assert.Len(t, entries, numRequests, "Expected all requests to be logged")

145-156: Add server readiness verification after the retry loop.

The loop attempts 30 retries but doesn't verify success after completing. If the server fails to start, tests will proceed and fail with confusing errors instead of a clear failure message.

🔧 Proposed fix
 	// Wait for server to be ready
 	client := &http.Client{Timeout: 2 * time.Second}
+	serverReady := false
 	for i := 0; i < 30; i++ {
 		resp, err := client.Get(serverURL + "/health")
 		if err == nil {
 			_ = resp.Body.Close()
 			if resp.StatusCode == http.StatusOK {
+				serverReady = true
 				break
 			}
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
+	require.True(t, serverReady, "server did not become ready at %s within expected time", serverURL)
 
 	return serverURL, srv, logger
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2309ac5 and a44a08b.

📒 Files selected for processing (6)
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/stream_wrapper.go
  • internal/server/handlers.go
  • internal/server/handlers_test.go
  • internal/server/http.go
  • tests/e2e/auditlog_test.go
🧰 Additional context used
📓 Path-based instructions (4)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/server/http.go
  • internal/server/handlers.go
  • tests/e2e/auditlog_test.go
  • internal/server/handlers_test.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/stream_wrapper.go
internal/server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use Echo (v4) framework for HTTP server setup and middleware, not Chi or other frameworks

Files:

  • internal/server/http.go
  • internal/server/handlers.go
  • internal/server/handlers_test.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • tests/e2e/auditlog_test.go
  • internal/server/handlers_test.go
  • internal/auditlog/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/auditlog_test.go
🧠 Learnings (8)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/server/http.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • internal/server/handlers.go
  • tests/e2e/auditlog_test.go
  • internal/server/handlers_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/server/handlers.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • tests/e2e/auditlog_test.go
  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • tests/e2e/auditlog_test.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : The responses converter in `internal/providers/responses_converter.go` must handle the OpenAI→Responses stream conversion for all providers

Applied to files:

  • internal/auditlog/stream_wrapper.go
🧬 Code graph analysis (5)
internal/server/http.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/server/handlers.go (1)
  • NewHandler (22-27)
internal/auditlog/stream_wrapper.go (1)
  • IsModelInteractionPath (246-258)
internal/auditlog/middleware.go (1)
  • Middleware (34-154)
internal/server/handlers.go (5)
internal/core/interfaces.go (1)
  • RoutableProvider (30-39)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/middleware.go (2)
  • EnrichEntry (230-249)
  • EnrichEntryWithStream (268-280)
internal/core/types.go (1)
  • Model (56-61)
internal/auditlog/stream_wrapper.go (4)
  • MarkEntryAsStreaming (230-233)
  • GetStreamEntryFromContext (214-226)
  • CreateStreamEntry (164-198)
  • WrapStreamForLogging (155-160)
tests/e2e/auditlog_test.go (4)
internal/auditlog/auditlog.go (2)
  • LogEntry (28-45)
  • Config (121-143)
internal/server/http.go (2)
  • Config (25-32)
  • New (35-136)
internal/auditlog/logger.go (2)
  • Logger (13-20)
  • NewLogger (24-44)
internal/core/types.go (3)
  • ChatRequest (4-10)
  • Model (56-61)
  • Message (25-28)
internal/server/handlers_test.go (1)
internal/server/handlers.go (1)
  • NewHandler (22-27)
internal/auditlog/stream_wrapper.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/auditlog.go (3)
  • LogEntry (28-45)
  • Config (121-143)
  • LogData (49-80)
internal/core/types.go (2)
  • Usage (49-53)
  • Model (56-61)
internal/auditlog/middleware.go (1)
  • Usage (341-345)
🪛 ast-grep (0.40.5)
tests/e2e/auditlog_test.go

[warning] 111-111: "Detected a network listener listening on 0.0.0.0 or an empty string.
This could unexpectedly expose the server publicly as it binds to all
available interfaces. Instead, specify another IP address that is not
0.0.0.0 nor the empty string."
Context: net.Listen("tcp", ":0")
Note: [CWE-200] Exposure of Sensitive Information to an Unauthorized Actor [REFERENCES]
- https://owasp.org/Top10/A01_2021-Broken_Access_Control

(avoid-bind-to-all-interfaces-go)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (38)
internal/auditlog/stream_wrapper.go (8)

11-21: LGTM on StreamLogWrapper struct design.

The struct appropriately embeds io.ReadCloser and maintains necessary state for capturing usage data from SSE streams. The 8KB buffer cap (line 46) is a reasonable trade-off for memory usage.


56-82: Close method handles idempotency correctly.

The closed flag prevents double-close issues and ensures the underlying stream is closed exactly once. Duration calculation and usage parsing are properly sequenced before the final write.


84-112: SSE parsing logic is correct for OpenAI-compatible streams.

The reverse iteration through events correctly finds usage data in the last non-[DONE] event. The splitting on \n\n follows SSE specification.


114-151: Using map[string]interface{} for JSON parsing.

Per coding guidelines, strongly-typed structs are preferred. This was already flagged as a nitpick in a previous review. The current approach is pragmatic for handling responses from various providers with potentially different structures.


153-160: WrapStreamForLogging guard conditions are appropriate.

Returns original stream when logging is disabled, preventing unnecessary overhead.


162-198: CreateStreamEntry shallow-copies pointer fields.

The Temperature and MaxTokens pointer fields are copied directly (lines 189-190), as noted in a previous review. This is typically safe since these values shouldn't change after creation.


200-210: copyMap correctly creates a shallow copy of header maps.

This prevents the copied entry from sharing the same map reference as the original.


212-243: Context utilities use interface constraints for flexibility.

Using interface{ Get(string) interface{} } allows these functions to work with Echo context and potentially other implementations. The streaming marker key concatenation is simple but effective.

internal/server/http.go (4)

24-32: Config struct additions are well-structured.

The new fields for audit logging are clearly named and documented. Using LoggerInterface allows for dependency injection and testing flexibility.


39-45: Logger initialization handles nil config safely.

The nil check on cfg before accessing AuditLogger prevents panics.


67-101: Logging format inconsistency between branches.

When LogOnlyModelInteractions is true, structured slog.Info output with specific fields is used. When false, middleware.RequestLogger() produces different default formatting. This was noted in a previous review as a nitpick about log aggregation consistency.


111-114: Audit middleware placement before authentication is intentional.

Placing audit logging before authentication captures all requests including failed auth attempts, which is typically desired for security audit trails. The comment documents this intent.

internal/server/handlers.go (4)

15-27: Handler struct and constructor updated correctly for audit logging.

The logger field is appropriately typed as auditlog.LoggerInterface, allowing nil for disabled logging.


40-42: Audit log enrichment before streaming is correct.

Model and provider type are captured before the streaming path diverges, ensuring metadata is available regardless of the response type.


44-81: Streaming audit logging integration is well-structured.

The sequence is correct:

  1. Mark as streaming to prevent middleware double-logging
  2. Get stream from provider
  3. Create stream entry copy
  4. Wrap stream for logging
  5. Set response headers
  6. Capture headers on entry after setting them
  7. Write response

The defer ensures the wrapped stream is closed even on panics.


57-59: StatusCode hardcoded to 200 may not reflect mid-stream errors.

Setting streamEntry.StatusCode = http.StatusOK assumes success. If the upstream provider sends an error mid-stream, this won't be captured. However, SSE streams typically start with 200 and errors are conveyed in the event data, so this is acceptable for most cases.

Consider whether mid-stream errors from providers should be captured differently, or if this is acceptable given SSE semantics.

Also applies to: 140-142

internal/server/handlers_test.go (2)

35-41: GetProviderType mock implementation is correct.

Returns "mock" when the model is supported, empty string otherwise, matching the expected interface contract.


101-101: All NewHandler calls consistently updated.

All test call sites pass nil as the second argument, which is appropriate for unit tests not exercising audit logging functionality.

Also applies to: 133-133, 165-165, 198-198, 240-240, 273-273, 303-303, 336-336, 369-369, 402-402, 435-435, 468-468, 497-497, 529-529

internal/auditlog/auditlog_test.go (14)

1-17: Test file imports and package setup are correct.

The package uses internal testing (package auditlog) which is appropriate for testing unexported functions like parseUsageFromSSE, hashAPIKey, and decompressBody.


19-113: TestRedactHeaders provides comprehensive coverage.

Tests cover nil input, empty input, no sensitive headers, single sensitive header, multiple sensitive headers, and case-insensitive matching. Good edge case coverage.


115-167: TestLogEntryJSON validates serialization round-trip.

Tests JSON marshaling and unmarshaling of LogEntry with nested LogData, verifying key fields are preserved.


169-215: TestLogDataWithBodies tests interface{} body handling.

This test validates that bodies stored as interface{} (maps) survive JSON round-trip, which is important for MongoDB storage compatibility mentioned in LogData comments.


217-252: mockStore implements thread-safe access.

The mutex and helper methods (getEntries, isClosed) address the race condition from a previous review.


274-281: Sleep-based synchronization is still present.

Using time.Sleep(200 * time.Millisecond) for test synchronization was flagged in a previous review as potentially flaky. Consider implementing a WaitForEntries helper as suggested.

Also applies to: 489-495


326-355: TestIsModelInteractionPath has comprehensive path coverage.

Tests model interaction paths, health/metrics paths, edge cases like empty strings and prefix-only matches. Good coverage of the path filtering logic.


357-433: TestParseUsageFromSSE validates SSE parsing logic.

Tests empty input, no usage data, usage data present, and usage in last non-DONE event. Covers the main parsing scenarios.


435-496: TestStreamLogWrapper validates end-to-end stream logging.

Tests the complete flow: wrap stream → read content → close → verify usage captured → verify entry logged. Good integration test.


498-513: TestWrapStreamForLogging tests guard conditions.

Verifies that original stream is returned when logger is nil or disabled.


515-573: TestCreateStreamEntry validates entry copying.

Tests nil input handling, field copying, stream flag setting, and importantly verifies that header maps are copied (not same reference) - this is a good isolation test.


575-633: TestHashAPIKey covers edge cases and consistency.

Tests empty header, Bearer-only, valid tokens, tokens without prefix, and verifies hash consistency and uniqueness.


635-658: Compression helper functions are correctly implemented.

compressGzip, compressDeflate, and compressBrotli use standard library and brotli package correctly for test data preparation.


660-774: TestDecompressBody provides thorough compression coverage.

Tests all supported encodings (gzip, deflate, brotli), case insensitivity, multiple encodings header format, unknown encodings, invalid data, and empty input. Excellent coverage.

tests/e2e/auditlog_test.go (6)

26-105: LGTM!

The mockLogStore implementation is well-designed with proper thread-safety using sync.Mutex, defensive copying in GetEntries(), and useful wait helpers for async testing.


160-376: LGTM!

The TestAuditLogMiddleware test suite comprehensively covers basic metadata capture, body/header logging, redaction, disabled logging, and API key hashing. All request payloads correctly use strongly-typed core.ChatRequest structs per coding guidelines.


378-515: LGTM!

The streaming tests thoroughly verify that audit logging captures streaming request metadata, response headers (including SSE-specific headers), and duration correctly.


573-629: LGTM!

Good parameterized test covering all sensitive headers that should be redacted. The isolation of creating a fresh server per header ensures clean test state.


631-704: LGTM!

Error capture tests properly verify that both unsupported model requests and invalid JSON requests are logged with correct status codes. The use of core.ChatRequest for the test payload correctly follows coding guidelines.


706-865: LGTM!

Comprehensive test coverage for the OnlyModelInteractions feature, including edge cases like mixed requests and proper handling of health check entries from server startup. The approach of counting entries before/after at lines 798-808 correctly handles the race condition with setup health checks.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +166 to +168
if r.body.Len() < 1024*1024 {
r.body.Write(b)
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error return from Write is ignored. While this is in a capture function where errors may be acceptable, the Write call can fail and return a non-nil error that should be logged or handled to detect issues with response capture.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +114
if cfg != nil && cfg.AuditLogger != nil && cfg.AuditLogger.Config().Enabled {
e.Use(auditlog.Middleware(cfg.AuditLogger))
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Enabled check is redundant since the middleware itself checks this condition at line 38 of middleware.go. Consider simplifying to: if cfg != nil && cfg.AuditLogger != nil { e.Use(auditlog.Middleware(cfg.AuditLogger)) }

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@docker-compose.yaml`:
- Around line 18-31: The env vars are inconsistent: LOGGING_STORAGE_TYPE is set
to mongodb while POSTGRES_URL is present; update the docker-compose env block to
avoid confusion by either commenting out or removing the unused POSTGRES_URL
when LOGGING_STORAGE_TYPE=mongodb, or conversely set
LOGGING_STORAGE_TYPE=postgresql if you intend to use POSTGRES_URL; alternatively
uncomment and populate MONGODB_URL if you want MongoDB and leave POSTGRES_URL
commented and add a short comment clarifying which DB is active (refer to
POSTGRES_URL, MONGODB_URL, and LOGGING_STORAGE_TYPE).

In `@internal/auditlog/auditlog_test.go`:
- Around line 272-279: Replace the timing-dependent time.Sleep in the test by
adding a polling helper on mockStore (e.g., func (m *mockStore)
WaitForEntries(count int, timeout time.Duration) bool) that repeatedly checks
m.entries under m.mu until the desired count or timeout, and then use that
helper in the test instead of Sleep and direct len(store.getEntries()) checks
(for example assert require.True(t, store.WaitForEntries(5,
500*time.Millisecond))). This keeps the test deterministic while reusing
mockStore and its mutex-protected entries access.

In `@internal/auditlog/logger.go`:
- Around line 46-68: Add a Prometheus counter to track dropped audit log entries
and increment it when the in-memory buffer is full: declare a package-level
counter (e.g., auditLogDroppedEntries using promauto.NewCounter with name
"gomodel_audit_log_dropped_entries_total") alongside other metrics (like
auditLogPartialWriteFailures), then update Logger.Write to call
auditLogDroppedEntries.Inc() in the default branch where the buffer is full
(right after the slog.Warn call) so drops are counted for observability and
compliance.
- Around line 114-131: The flushLoop closes l.buffer on shutdown which can race
with concurrent Write() calls and panic; add a shutdown guard (e.g., an atomic
boolean field like l.closed or a sync.Once) and set it in Close()/flushLoop
before closing l.buffer, and have Write() check that guard and return an error
(or drop) if shut down so no send occurs on a closed channel; also ensure
Close()/flushLoop use a single close path (sync.Once or atomic check) so
l.buffer is closed exactly once and cancel/Flush still run as before.

In `@internal/auditlog/store_postgresql.go`:
- Around line 102-130: writeBatchSmall currently swallows insert failures by
only logging warnings and always returning nil; change it to detect failures
(e.g., track a failure count or collect the first/aggregated error) while
iterating over entries and return a non-nil error at the end if any Exec calls
failed so behavior matches writeBatchLarge's transactional error semantics;
update PostgreSQLStore.writeBatchSmall to accumulate errors from pool.Exec (or
return the first error) and return a descriptive error (or wrap multiple errors)
instead of always returning nil.
- Around line 13-18: PostgreSQLStore has the same double-close panic risk as
SQLiteStore: add a sync.Once field (e.g., closeOnce) to the PostgreSQLStore
struct and update the Close() method to call closeOnce.Do(...) to perform
shutdown steps exactly once; inside the once block close the stopCleanup channel
(if non-nil) and call pool.Close(), and ensure any background cleanup goroutine
listens for stopCleanup to exit so repeated Close() calls are safe.

In `@internal/auditlog/store_sqlite.go`:
- Around line 179-196: The cleanup() method on SQLiteStore currently calls
s.db.Exec without a context; change it to use a context with a timeout (match
Postgres' 5-minute timeout) by creating ctx, cancel :=
context.WithTimeout(context.Background(), 5*time.Minute) and deferring cancel(),
then call s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE timestamp < ?",
cutoff) and keep the existing error and RowsAffected handling (result, err and
rowsAffected logic) so the delete cannot block indefinitely; update imports if
needed to include context and time.

In `@internal/auditlog/stream_wrapper.go`:
- Around line 250-263: IsModelInteractionPath currently hardcodes model
endpoints; change it to consult the audit log Config (e.g., add a Config field
like ModelInteractionPaths []string or a getter) so IsModelInteractionPath(path
string) reads the list from that Config (falling back to the existing defaults
"/v1/chat/completions", "/v1/responses", "/v1/models" when
Config.ModelInteractionPaths is empty), and update callers to pass or access the
Config instance used by the audit logger; ensure the function still uses
strings.HasPrefix for matching.
♻️ Duplicate comments (13)
internal/auditlog/store_sqlite.go (1)

151-158: Potential panic on double-close of stopCleanup channel.

If Close() is called multiple times, closing an already-closed channel will panic. Add protection using sync.Once.

🐛 Proposed fix using sync.Once
+import "sync"
+
 // SQLiteStore implements LogStore for SQLite databases.
 type SQLiteStore struct {
 	db            *sql.DB
 	retentionDays int
 	stopCleanup   chan struct{}
+	closeOnce     sync.Once
 }

 // Close stops the cleanup goroutine.
 // Note: We don't close the DB here as it's managed by the storage layer.
 func (s *SQLiteStore) Close() error {
-	if s.retentionDays > 0 {
-		close(s.stopCleanup)
-	}
+	if s.retentionDays > 0 {
+		s.closeOnce.Do(func() {
+			close(s.stopCleanup)
+		})
+	}
 	return nil
 }
internal/auditlog/store_postgresql.go (1)

93-94: Misleading comment: COPY is not actually used.

The comment states "Use COPY for better performance with large batches" but both implementations use individual INSERT statements. Update the comment to reflect the actual approach.

♻️ Proposed fix
-	// Use COPY for better performance with large batches
-	// But for smaller batches, use a simple INSERT
+	// For smaller batches, use individual INSERTs
+	// For larger batches, wrap INSERTs in a transaction for atomicity
internal/auditlog/auditlog.go (1)

98-121: Consider using a map for O(1) header lookup.

The current implementation has O(n×m) complexity. While acceptable for small header counts, a map-based lookup would be more efficient.

internal/auditlog/store_mongodb.go (2)

93-98: TTL calculation could overflow for extremely large retention periods.

While unlikely in practice (>68 years), the calculation retentionDays * 24 * 60 * 60 could overflow int32. Use int64 arithmetic before casting for defensive coding.

♻️ Proposed fix
-		ttlSeconds := int32(retentionDays * 24 * 60 * 60)
+		ttlSeconds := int32(int64(retentionDays) * 24 * 60 * 60)

105-109: Consider distinguishing recoverable vs critical index creation errors.

The current approach logs all index errors as warnings. This could mask critical errors (e.g., invalid TTL configuration, insufficient permissions). Consider checking for specific MongoDB error codes (85/86 for index conflicts) and failing fast on other errors.

internal/auditlog/middleware.go (3)

81-96: Unbounded request body read remains unaddressed.

The request body is still read without a size limit. While there's a 1MB limit for response body capture (line 166), request bodies can be arbitrarily large, risking memory exhaustion.

🐛 Proposed fix to limit request body capture
 // Capture request body if enabled
-if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
-	bodyBytes, err := io.ReadAll(req.Body)
+const maxRequestBodyCapture int64 = 1024 * 1024 // 1MB
+if cfg.LogBodies && req.Body != nil && req.ContentLength > 0 {
+	limitReader := io.LimitReader(req.Body, maxRequestBodyCapture)
+	bodyBytes, err := io.ReadAll(limitReader)
 	if err == nil {
 		// Parse JSON to interface{} for native BSON storage in MongoDB
 		var parsed interface{}
 		if jsonErr := json.Unmarshal(bodyBytes, &parsed); jsonErr == nil {
 			entry.Data.RequestBody = parsed
 		} else {
 			// Fallback: store as valid UTF-8 string if not valid JSON
 			entry.Data.RequestBody = toValidUTF8String(bodyBytes)
 		}
-		// Restore the body for the handler
-		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+		// Restore the full body for the handler
+		if req.ContentLength > maxRequestBodyCapture {
+			remaining, _ := io.ReadAll(req.Body)
+			bodyBytes = append(bodyBytes, remaining...)
+		}
+		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
 	}
 }

192-212: Duplicate functions should be consolidated.

extractHeaders and extractEchoHeaders have identical implementations. This violates DRY principles.

♻️ Proposed fix to remove duplication
-// extractHeaders extracts headers from http.Header, redacting sensitive ones
+// extractHeaders extracts headers from a header map (http.Header or echo response headers),
+// taking only the first value of each key and redacting sensitive headers.
 func extractHeaders(headers map[string][]string) map[string]string {
 	result := make(map[string]string, len(headers))
 	for key, values := range headers {
 		if len(values) > 0 {
 			result[key] = values[0]
 		}
 	}
 	return RedactHeaders(result)
 }
-
-// extractEchoHeaders extracts headers from echo's header map
-func extractEchoHeaders(headers map[string][]string) map[string]string {
-	result := make(map[string]string, len(headers))
-	for key, values := range headers {
-		if len(values) > 0 {
-			result[key] = values[0]
-		}
-	}
-	return RedactHeaders(result)
-}

Then update line 122:

-entry.Data.ResponseHeaders = extractEchoHeaders(c.Response().Header())
+entry.Data.ResponseHeaders = extractHeaders(c.Response().Header())

164-171: Extract magic number to named constant for consistency.

The 1MB limit for response body capture should be a named constant for clarity, similar to maxDecompressedSize on line 312.

♻️ Suggested improvement
+const maxResponseBodyCapture = 1024 * 1024 // 1MB

 func (r *responseBodyCapture) Write(b []byte) (int, error) {
 	// Write to the capture buffer (limit to 1MB to avoid memory issues)
-	if r.body.Len() < 1024*1024 {
+	if r.body.Len() < maxResponseBodyCapture {
 		r.body.Write(b)
 	}
 	// Write to the original response writer
 	return r.ResponseWriter.Write(b)
 }
docker-compose.yaml (1)

72-84: MongoDB service lacks authentication credentials.

The MongoDB service is exposed on port 27017 without authentication. While acceptable for local development, this poses a security risk if this configuration is used as a template for production deployments.

🔒 Suggested fix to add authentication
   mongodb:
     image: mongo:8
     ports:
       - "27017:27017"
+    environment:
+      - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER:-admin}
+      - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD:-changeme}
     volumes:
       - mongodb_data:/data/db
     restart: unless-stopped

Then update the MongoDB connection URL if used:

-      # - MONGODB_URL=mongodb://mongodb:27017/gomodel
+      # - MONGODB_URL=mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-changeme}@mongodb:27017/gomodel?authSource=admin
tests/e2e/auditlog_test.go (2)

558-569: Consider tightening concurrency test assertion.

The test allows up to 2 missing entries (numRequests-2). With a buffer size of 1000 and only 20 concurrent requests, all entries should be logged. This tolerance may mask actual bugs.

♻️ Suggested tightening
 		// Wait for all log entries
 		entries := store.WaitForAPIEntries(numRequests, 5*time.Second)
-		assert.GreaterOrEqual(t, len(entries), numRequests-2, "Expected most requests to be logged")
+		assert.Len(t, entries, numRequests, "Expected all requests to be logged")

If flakiness occurs in CI, the root cause should be investigated rather than tolerated.


144-158: Add server readiness verification after the retry loop.

The server readiness check loops 30 times but doesn't verify the server actually started after the loop. If the server fails to start, subsequent tests will fail with confusing errors.

🐛 Proposed fix
 	// Wait for server to be ready
 	client := &http.Client{Timeout: 2 * time.Second}
+	serverReady := false
 	for i := 0; i < 30; i++ {
 		resp, err := client.Get(serverURL + "/health")
 		if err == nil {
 			_ = resp.Body.Close()
 			if resp.StatusCode == http.StatusOK {
+				serverReady = true
 				break
 			}
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
+	require.True(t, serverReady, "server did not become ready within 3 seconds")

 	return serverURL, srv, logger
internal/auditlog/stream_wrapper.go (2)

118-155: Consider using strongly-typed struct for usage extraction.

Per coding guidelines, strongly-typed structs are preferred over map[string]interface{}. While the multi-provider rationale is valid, a minimal typed struct improves safety.

♻️ Suggested typed approach
// chunkWithUsage represents the common OpenAI-compatible chunk format
type chunkWithUsage struct {
	Usage *Usage `json:"usage"`
}

func extractUsageFromJSON(data []byte) *Usage {
	var chunk chunkWithUsage
	if err := json.Unmarshal(data, &chunk); err != nil {
		return nil
	}
	if chunk.Usage == nil {
		return nil
	}
	if chunk.Usage.PromptTokens > 0 || chunk.Usage.CompletionTokens > 0 || chunk.Usage.TotalTokens > 0 {
		return chunk.Usage
	}
	return nil
}

Based on learnings, strongly-typed structs are preferred for catching errors at compile time.


166-203: Shallow copy of pointer fields in CreateStreamEntry.

Temperature and MaxTokens pointer fields are copied by reference, meaning both the original and copied entry share the same underlying values. While typically safe since these values are read-only after creation, for true isolation consider deep copying.

♻️ Optional: Deep copy pointer fields
 	if baseEntry.Data != nil {
 		entryCopy.Data = &LogData{
 			UserAgent:       baseEntry.Data.UserAgent,
 			APIKeyHash:      baseEntry.Data.APIKeyHash,
-			Temperature:     baseEntry.Data.Temperature,
-			MaxTokens:       baseEntry.Data.MaxTokens,
+			Temperature:     copyFloat64Ptr(baseEntry.Data.Temperature),
+			MaxTokens:       copyIntPtr(baseEntry.Data.MaxTokens),
 			RequestHeaders:  copyMap(baseEntry.Data.RequestHeaders),
 			ResponseHeaders: copyMap(baseEntry.Data.ResponseHeaders),
 			RequestBody:     baseEntry.Data.RequestBody,
 		}
 	}

Add helper functions:

func copyFloat64Ptr(p *float64) *float64 {
	if p == nil { return nil }
	v := *p
	return &v
}

func copyIntPtr(p *int) *int {
	if p == nil { return nil }
	v := *p
	return &v
}
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a44a08b and 2378bbe.

📒 Files selected for processing (10)
  • docker-compose.yaml
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/stream_wrapper.go
  • tests/e2e/auditlog_test.go
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/logger.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
  • tests/e2e/auditlog_test.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • internal/auditlog/auditlog_test.go
  • tests/e2e/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/auditlog_test.go
🧠 Learnings (8)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • internal/auditlog/auditlog_test.go
  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • internal/auditlog/stream_wrapper.go
  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : The responses converter in `internal/providers/responses_converter.go` must handle the OpenAI→Responses stream conversion for all providers

Applied to files:

  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • tests/e2e/auditlog_test.go
🧬 Code graph analysis (9)
internal/auditlog/store_sqlite.go (3)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
internal/core/types.go (1)
  • Model (56-61)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/auditlog/store_mongodb.go (2)
internal/storage/storage.go (1)
  • New (82-93)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
internal/auditlog/auditlog.go (2)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/server/http.go (1)
  • Config (25-32)
internal/auditlog/logger.go (1)
internal/auditlog/auditlog.go (3)
  • LogStore (13-24)
  • Config (124-146)
  • LogEntry (28-56)
internal/auditlog/store_postgresql.go (3)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
internal/core/types.go (1)
  • Model (56-61)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/auditlog/auditlog_test.go (3)
internal/auditlog/auditlog.go (4)
  • RedactHeaders (100-121)
  • LogEntry (28-56)
  • LogData (61-83)
  • Config (124-146)
internal/auditlog/logger.go (1)
  • NewLogger (24-44)
internal/auditlog/stream_wrapper.go (4)
  • IsModelInteractionPath (251-263)
  • NewStreamLogWrapper (25-37)
  • WrapStreamForLogging (159-164)
  • CreateStreamEntry (168-203)
internal/auditlog/stream_wrapper.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (169-173)
internal/auditlog/auditlog.go (3)
  • LogEntry (28-56)
  • Config (124-146)
  • LogData (61-83)
internal/auditlog/middleware.go (2)
  • Usage (343-347)
  • LogEntryKey (28-28)
internal/core/types.go (2)
  • Usage (49-53)
  • Model (56-61)
internal/auditlog/middleware.go (2)
internal/auditlog/auditlog.go (2)
  • LogEntry (28-56)
  • RedactHeaders (100-121)
internal/auditlog/stream_wrapper.go (2)
  • IsModelInteractionPath (251-263)
  • IsEntryMarkedAsStreaming (241-248)
tests/e2e/auditlog_test.go (4)
internal/auditlog/auditlog.go (2)
  • LogEntry (28-56)
  • Config (124-146)
internal/server/http.go (2)
  • Config (25-32)
  • New (35-136)
internal/auditlog/logger.go (2)
  • Logger (13-20)
  • NewLogger (24-44)
internal/core/types.go (3)
  • ChatRequest (4-10)
  • Model (56-61)
  • Message (25-28)
🪛 ast-grep (0.40.5)
tests/e2e/auditlog_test.go

[warning] 111-111: "Detected a network listener listening on 0.0.0.0 or an empty string.
This could unexpectedly expose the server publicly as it binds to all
available interfaces. Instead, specify another IP address that is not
0.0.0.0 nor the empty string."
Context: net.Listen("tcp", ":0")
Note: [CWE-200] Exposure of Sensitive Information to an Unauthorized Actor [REFERENCES]
- https://owasp.org/Top10/A01_2021-Broken_Access_Control

(avoid-bind-to-all-interfaces-go)

🪛 Checkov (3.2.334)
docker-compose.yaml

[medium] 19-20: Basic Auth Credentials

(CKV_SECRET_4)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (29)
internal/auditlog/store_sqlite.go (3)

13-18: LGTM!

The struct definition is clean and follows Go conventions. The use of a channel for cleanup coordination aligns with the coding guidelines for goroutine-based concurrency.


20-82: LGTM!

The constructor properly validates input, creates the table schema matching LogEntry fields, and handles index creation errors gracefully. The conditional cleanup goroutine start based on retentionDays is appropriate.


84-144: LGTM!

The batch insert implementation is well-structured with proper JSON marshaling, bool-to-int conversion for SQLite, and INSERT OR IGNORE for idempotent writes. The use of interface{} for SQL parameter values is acceptable here as it's required by the database/sql API.

internal/auditlog/store_postgresql.go (3)

20-85: LGTM!

The constructor properly validates input, creates an appropriate PostgreSQL schema with UUID, TIMESTAMPTZ, and JSONB types, and includes a GIN index on the data column for efficient JSON queries.


132-171: LGTM!

The transaction-based batch insert properly handles begin/commit/rollback with a deferred rollback for safety. The ON CONFLICT DO NOTHING clause ensures idempotency.


206-226: LGTM!

The cleanup implementation properly uses a timeout context (5 minutes) and handles errors appropriately. Good consistency with the cleanup pattern.

internal/auditlog/auditlog.go (3)

11-24: LGTM!

The LogStore interface is well-defined with clear method documentation and thread-safety requirements. The three methods (WriteBatch, Flush, Close) provide a clean contract for storage backends.


78-83: Consider the trade-off of using interface{} for body fields.

Per coding guidelines, strongly-typed structs are preferred over interface{}. While the comment explains this enables native BSON documents in MongoDB, it sacrifices compile-time type safety. Consider whether a typed wrapper or generic constraint could provide similar flexibility with better type guarantees.

This is an architectural decision - verify if the flexibility of arbitrary JSON structures from different AI providers justifies the loss of type safety.


123-159: LGTM!

The Config struct and DefaultConfig() function provide sensible defaults with audit logging disabled by default (opt-in). The buffer size (1000), flush interval (5s), and retention (30 days) are reasonable production defaults.

internal/auditlog/store_mongodb.go (2)

17-44: LGTM!

The PartialWriteError type properly wraps mongo.BulkWriteException with additional context (total entries, failed count). The Prometheus counter gomodel_audit_log_partial_write_failures_total enables operators to monitor data loss. The Unwrap() method correctly returns ErrPartialWrite for errors.Is checks.


117-155: LGTM!

The WriteBatch implementation properly handles partial write failures by returning a PartialWriteError with context and incrementing the Prometheus counter. The unordered insert (SetOrdered(false)) improves throughput by continuing on individual failures.

internal/auditlog/logger.go (3)

10-44: LGTM!

The Logger struct properly uses channels and sync.WaitGroup for concurrent operation, aligning with the coding guidelines for goroutine-based concurrency. The NewLogger constructor applies sensible defaults for BufferSize and FlushInterval.


135-150: LGTM!

The flushBatch method properly uses a timeout context (30s) and logs errors without propagating them, which is appropriate for a background flush operation.


152-173: LGTM!

The NoopLogger and LoggerInterface provide a clean abstraction for disabled logging scenarios and enable polymorphism between real and no-op implementations.

internal/auditlog/middleware.go (3)

228-249: Missing nil check for entry.Data in EnrichEntry.

EnrichEntry accesses entry.PromptTokens etc. directly on the entry (not entry.Data), which is fine. However, the function signature and pattern differs from EnrichEntryWithError which does check entry.Data. Looking at the code more carefully, I see that token fields are now on LogEntry directly (not nested in Data), so this is actually correct.


156-190: Good implementation of http.Flusher and http.Hijacker interfaces.

The responseBodyCapture wrapper correctly implements both Flush() and Hijack() by delegating to the underlying ResponseWriter. This ensures SSE streaming and WebSocket upgrades work correctly with the audit logging wrapper.


296-340: Well-implemented decompression with compression bomb protection.

The decompressBody function correctly:

  • Handles multiple encodings (gzip, deflate, brotli)
  • Limits decompressed size to 2MB to prevent compression bomb attacks
  • Gracefully falls back to original body on failure
internal/auditlog/stream_wrapper.go (2)

39-58: Buffer error handling correctly implemented.

The Read method now properly checks for buffer write errors (lines 44-46 and 52-54), addressing the previous concern about ignoring write errors.


60-86: Clean implementation of stream close with usage extraction.

The Close method correctly:

  • Uses idempotent close pattern with w.closed flag
  • Calculates duration from the original request start time
  • Parses usage from buffered SSE data
  • Writes the completed log entry
docker-compose.yaml (1)

33-38: Good use of healthcheck dependency for service ordering.

The gomodel service correctly uses condition: service_healthy for both Redis and PostgreSQL dependencies, ensuring the application only starts after databases are ready.

internal/auditlog/auditlog_test.go (4)

216-250: Thread-safe mockStore implementation.

The mockStore correctly uses sync.Mutex for all operations, with safe accessor methods getEntries() and isClosed(). This addresses the previous race condition concerns.


19-113: Comprehensive header redaction tests.

Excellent coverage of edge cases including:

  • Nil/empty headers
  • Case-insensitive matching
  • Multiple sensitive headers
  • Non-sensitive header preservation

669-750: Thorough decompression test coverage.

Good coverage of all compression encodings (gzip, deflate, brotli) with edge cases for:

  • Empty/identity encoding
  • Case variations
  • Multiple encodings (takes first)
  • Unknown encodings
  • Invalid compressed data

584-642: Good API key hashing tests.

Tests cover important scenarios:

  • Empty/whitespace inputs
  • With/without Bearer prefix
  • Consistency (same input → same hash)
  • Uniqueness (different inputs → different hashes)
  • Hash length verification (8 chars)
tests/e2e/auditlog_test.go (5)

1-3: Correct e2e build tag usage.

The file properly uses //go:build e2e as required by coding guidelines for E2E tests.


111-115: Dynamic port binding is safe for tests.

The static analysis warning about binding to all interfaces is a false positive for test code. Using :0 to get a dynamic port is the correct pattern for avoiding port conflicts in parallel tests.


179-202: Well-structured test using strongly-typed request payloads.

The test correctly uses core.ChatRequest with core.Message structs instead of map[string]interface{}, complying with coding guidelines. The assertions verify all key metadata fields.


705-863: Excellent test coverage for OnlyModelInteractions filtering.

The tests thoroughly verify:

  • Model endpoints are logged when enabled
  • Health endpoints are skipped when enabled
  • Health endpoints are logged when disabled
  • Mixed request filtering works correctly

This ensures the filtering logic is robust.


25-105: Well-designed mock log store for e2e tests.

The mockLogStore implementation provides:

  • Thread-safe operations with mutex
  • Separate methods for all entries vs API entries
  • Wait helpers with timeout for async verification
  • Clean separation of concerns

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +46 to +68
// Write queues a log entry for async writing.
// This method is non-blocking. If the buffer is full, the entry is dropped
// and a warning is logged.
func (l *Logger) Write(entry *LogEntry) {
if entry == nil {
return
}

select {
case l.buffer <- entry:
// Entry queued successfully
default:
// Buffer full - drop entry and log warning
requestID := entry.RequestID
if requestID == "" {
requestID = "unknown"
}
slog.Warn("audit log buffer full, dropping entry",
"request_id", requestID,
"model", entry.Model,
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding a metric counter for dropped entries.

When the buffer is full, entries are dropped with only a log warning. For compliance and observability, consider adding a Prometheus counter to track dropped entries, similar to auditLogPartialWriteFailures in the MongoDB store.

♻️ Proposed addition
var auditLogDroppedEntries = promauto.NewCounter(
	prometheus.CounterOpts{
		Name: "gomodel_audit_log_dropped_entries_total",
		Help: "Total number of audit log entries dropped due to buffer full",
	},
)

// In Write method, after logging warning:
auditLogDroppedEntries.Inc()
🤖 Prompt for AI Agents
In `@internal/auditlog/logger.go` around lines 46 - 68, Add a Prometheus counter
to track dropped audit log entries and increment it when the in-memory buffer is
full: declare a package-level counter (e.g., auditLogDroppedEntries using
promauto.NewCounter with name "gomodel_audit_log_dropped_entries_total")
alongside other metrics (like auditLogPartialWriteFailures), then update
Logger.Write to call auditLogDroppedEntries.Inc() in the default branch where
the buffer is full (right after the slog.Warn call) so drops are counted for
observability and compliance.

Comment on lines +179 to +196
// cleanup deletes log entries older than the retention period.
func (s *SQLiteStore) cleanup() {
if s.retentionDays <= 0 {
return
}

cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)

result, err := s.db.Exec("DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
if err != nil {
slog.Error("failed to cleanup old audit logs", "error", err)
return
}

if rowsAffected, err := result.RowsAffected(); err == nil && rowsAffected > 0 {
slog.Info("cleaned up old audit logs", "deleted", rowsAffected)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding a timeout context for the cleanup query.

The cleanup() method executes a DELETE query without a timeout context, unlike the PostgreSQL implementation which uses a 5-minute timeout. For large tables with many old entries, this could block indefinitely.

♻️ Proposed fix
 func (s *SQLiteStore) cleanup() {
 	if s.retentionDays <= 0 {
 		return
 	}

+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+	defer cancel()
+
 	cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)

-	result, err := s.db.Exec("DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
+	result, err := s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
 	if err != nil {
 		slog.Error("failed to cleanup old audit logs", "error", err)
 		return
 	}

 	if rowsAffected, err := result.RowsAffected(); err == nil && rowsAffected > 0 {
 		slog.Info("cleaned up old audit logs", "deleted", rowsAffected)
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// cleanup deletes log entries older than the retention period.
func (s *SQLiteStore) cleanup() {
if s.retentionDays <= 0 {
return
}
cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)
result, err := s.db.Exec("DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
if err != nil {
slog.Error("failed to cleanup old audit logs", "error", err)
return
}
if rowsAffected, err := result.RowsAffected(); err == nil && rowsAffected > 0 {
slog.Info("cleaned up old audit logs", "deleted", rowsAffected)
}
}
// cleanup deletes log entries older than the retention period.
func (s *SQLiteStore) cleanup() {
if s.retentionDays <= 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)
result, err := s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
if err != nil {
slog.Error("failed to cleanup old audit logs", "error", err)
return
}
if rowsAffected, err := result.RowsAffected(); err == nil && rowsAffected > 0 {
slog.Info("cleaned up old audit logs", "deleted", rowsAffected)
}
}
🤖 Prompt for AI Agents
In `@internal/auditlog/store_sqlite.go` around lines 179 - 196, The cleanup()
method on SQLiteStore currently calls s.db.Exec without a context; change it to
use a context with a timeout (match Postgres' 5-minute timeout) by creating ctx,
cancel := context.WithTimeout(context.Background(), 5*time.Minute) and deferring
cancel(), then call s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE
timestamp < ?", cutoff) and keep the existing error and RowsAffected handling
(result, err and rowsAffected logic) so the delete cannot block indefinitely;
update imports if needed to include context and time.

Comment on lines +250 to +263
// IsModelInteractionPath returns true if the path is an AI model endpoint
func IsModelInteractionPath(path string) bool {
modelPaths := []string{
"/v1/chat/completions",
"/v1/responses",
"/v1/models",
}
for _, p := range modelPaths {
if strings.HasPrefix(path, p) {
return true
}
}
return false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Hardcoded model paths could be configurable.

IsModelInteractionPath hardcodes the list of model endpoints. Consider making this configurable via the audit log Config for flexibility.

This was noted in a past review comment and remains a minor improvement opportunity.

🤖 Prompt for AI Agents
In `@internal/auditlog/stream_wrapper.go` around lines 250 - 263,
IsModelInteractionPath currently hardcodes model endpoints; change it to consult
the audit log Config (e.g., add a Config field like ModelInteractionPaths
[]string or a getter) so IsModelInteractionPath(path string) reads the list from
that Config (falling back to the existing defaults "/v1/chat/completions",
"/v1/responses", "/v1/models" when Config.ModelInteractionPaths is empty), and
update callers to pass or access the Config instance used by the audit logger;
ensure the function still uses strings.HasPrefix for matching.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docker-compose.yaml`:
- Around line 32-36: The depends_on block currently ensures postgres is healthy
before starting but does not include mongodb; add an entry for mongodb under
depends_on with condition: service_healthy (matching the postgres entry) so the
service will wait for mongodb to be healthy when LOGGING_STORAGE_TYPE=mongodb;
update the depends_on block (the section containing redis, postgres) to also
list mongodb with the same condition to ensure correct startup ordering.
♻️ Duplicate comments (1)
docker-compose.yaml (1)

19-25: Critical: MongoDB URL is commented out but storage type is set to mongodb.

The configuration sets LOGGING_STORAGE_TYPE=mongodb (line 25) but MONGODB_URL is commented out (line 21). This will cause the application to fail at runtime when attempting to connect to MongoDB for audit logging.

Either uncomment the MongoDB URL or switch the storage type to PostgreSQL:

🔧 Option 1: Use MongoDB (uncomment URL)
       # Storage configuration (PostgreSQL for audit logs)
-      - POSTGRES_URL=postgres://gomodel:gomodel@postgres:5432/gomodel
+      # - POSTGRES_URL=postgres://gomodel:gomodel@postgres:5432/gomodel
       # MongoDB configuration (uncomment to use MongoDB instead)
-      # - MONGODB_URL=mongodb://mongodb:27017/gomodel
+      - MONGODB_URL=mongodb://mongodb:27017/gomodel
       # Audit logging - all enabled with PostgreSQL
       - LOGGING_ENABLED=true
-      # - LOGGING_STORAGE_TYPE=postgresql
       - LOGGING_STORAGE_TYPE=mongodb
🔧 Option 2: Use PostgreSQL (change storage type)
       # Audit logging - all enabled with PostgreSQL
       - LOGGING_ENABLED=true
-      # - LOGGING_STORAGE_TYPE=postgresql
-      - LOGGING_STORAGE_TYPE=mongodb
+      - LOGGING_STORAGE_TYPE=postgresql
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2378bbe and b55bab9.

📒 Files selected for processing (1)
  • docker-compose.yaml
🧰 Additional context used
🪛 Checkov (3.2.334)
docker-compose.yaml

[medium] 19-20: Basic Auth Credentials

(CKV_SECRET_4)

🔇 Additional comments (3)
docker-compose.yaml (3)

85-91: Adminer configuration looks good for development.

The Adminer service provides a useful database management UI for development. Consider also adding MongoDB to depends_on if you want to use Adminer to inspect MongoDB (though Adminer has limited MongoDB support).


108-112: LGTM!

Volume declarations for postgres_data and mongodb_data properly persist database data across container restarts.


53-54: No action needed. The postgres:18-alpine image exists and is valid. PostgreSQL 18 was officially released on September 25, 2025, making it the current latest major version as of January 2026. The Dockerfile correctly uses an available image tag.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@docker-compose.yaml`:
- Around line 19-20: The compose file contains hardcoded DB credentials in
POSTGRES_URL and the postgres service env; remove those inline secrets and
instead reference environment variables sourced from the existing .env (e.g. use
POSTGRES_URL=${POSTGRES_URL} or split into POSTGRES_USER=${POSTGRES_USER},
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}, POSTGRES_DB=${POSTGRES_DB}), ensure the
compose uses env_file or variable substitution, and add a fail-fast check (or
document) to require these variables be set before startup; also apply the same
replacement to the other occurrences of hardcoded creds (the postgres service
env block and the repeated entries referenced later).
- Around line 26-30: The docker-compose defaults currently enable full
header/body logging via LOGGING_LOG_BODIES and LOGGING_LOG_HEADERS which can
expose credentials/PII; change those two variables to false in
docker-compose.yaml (make them opt-in), and add documentation or an environment
flag requiring explicit opt‑in plus guidance to enable redaction/allow‑listing
before turning them on; keep LOGGING_BUFFER_SIZE, LOGGING_FLUSH_INTERVAL, and
LOGGING_RETENTION_DAYS as-is but ensure any runtime config validation (if
present) checks that full body/header logging cannot be enabled without
redaction settings.

In `@internal/auditlog/auditlog.go`:
- Around line 58-87: LogData currently uses interface{} for RequestBody and
ResponseBody; replace those with concrete typed structs (e.g., define
RequestPayload and ResponsePayload or a single BodyPayload typed union struct)
and change the LogData fields RequestBody and ResponseBody to those types with
the same json/bson tags. Update any code that constructs LogData (call sites
creating RequestBody/ResponseBody) to populate the new typed struct fields and
handle any variants (e.g., raw JSON, form data, multipart) via explicit fields
on the payload structs rather than interface{}. Ensure the new types are
serializable by MongoDB (bson/json tags) and preserve the
RequestBodyTooBigToHandle/ResponseBodyTruncated semantics.

In `@internal/auditlog/middleware.go`:
- Around line 222-234: The hashAPIKey function currently returns the first 8 hex
chars of the SHA256 of the token, which risks collisions; update hashAPIKey to
return a longer prefix (recommend 16 hex chars) or make the prefix length
configurable via a constant or parameter (e.g., HashPrefixLength) and use that
constant when slicing hex.EncodeToString(hash[:]); ensure the function still
handles empty tokens the same way and that any callers (e.g., audit log writers)
continue to accept the longer prefix.
- Around line 92-99: Replace the untyped json.Unmarshal into interface{} with a
typed-first approach: define strongly-typed structs for known payloads (e.g.,
ChatCompletionRequest with fields like Model and Messages), then in the
middleware try json.Unmarshal(bodyBytes, &chatReq) and validate a required field
(e.g., chatReq.Model != "") before assigning entry.Data.RequestBody = chatReq;
if that fails fall back to the existing behavior (entry.Data.RequestBody =
toValidUTF8String(bodyBytes)). Keep json.Unmarshal and toValidUTF8String usage,
and ensure assignment targets entry.Data.RequestBody so existing downstream code
still works.

In `@tests/e2e/auditlog_test.go`:
- Around line 111-142: The test binds the server to all interfaces using ":0"
and ":%d"; change those to bind only to the loopback address by replacing
net.Listen("tcp", ":0") with net.Listen("tcp", "127.0.0.1:0") and start the
server with srv.Start(fmt.Sprintf("127.0.0.1:%d", port)); also update any client
URL construction (serverURL) to use "http://127.0.0.1:%d" to match the bind
address so the test only listens on localhost. Ensure references are updated for
listener, port, srv.Start and serverURL (fmt.Sprintf usages).
♻️ Duplicate comments (7)
docker-compose.yaml (1)

73-78: MongoDB auth is still missing.

This service exposes MongoDB without credentials. Please add MONGO_INITDB_ROOT_USERNAME / MONGO_INITDB_ROOT_PASSWORD and update the connection URL accordingly.

internal/auditlog/store_sqlite.go (1)

184-200: Add a timeout context to cleanup deletes.
The cleanup query can run indefinitely on large tables; Postgres already uses a 5‑minute timeout.

♻️ Proposed fix
 func (s *SQLiteStore) cleanup() {
 	if s.retentionDays <= 0 {
 		return
 	}
 
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+	defer cancel()
+
 	cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)
 
-	result, err := s.db.Exec("DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
+	result, err := s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
 	if err != nil {
 		slog.Error("failed to cleanup old audit logs", "error", err)
 		return
 	}
internal/auditlog/store_mongodb.go (1)

90-98: Guard TTL seconds against int32 overflow.

Large retention periods can overflow int32 when converting days → seconds, producing a negative/invalid TTL. Please clamp or validate before casting.

🛠️ Proposed fix (clamp to MaxInt32)
 	if retentionDays > 0 {
-		ttlSeconds := int32(int64(retentionDays) * 24 * 60 * 60)
+		ttlSeconds64 := int64(retentionDays) * 24 * 60 * 60
+		if ttlSeconds64 > math.MaxInt32 {
+			ttlSeconds64 = math.MaxInt32
+		}
+		ttlSeconds := int32(ttlSeconds64)
 		indexes = append(indexes, mongo.IndexModel{
 			Keys:    bson.D{{Key: "timestamp", Value: -1}},
 			Options: options.Index().SetExpireAfterSeconds(ttlSeconds),
 		})
 	} else {
 import (
 	"context"
 	"errors"
 	"fmt"
 	"log/slog"
+	"math"
 	"time"
tests/e2e/auditlog_test.go (1)

144-155: Assert server readiness after the polling loop.

If the server doesn’t start within the loop, the tests proceed and fail later with unclear errors.

🛠️ Proposed fix
 	// Wait for server to be ready
 	client := &http.Client{Timeout: 2 * time.Second}
+	serverReady := false
 	for i := 0; i < 30; i++ {
 		resp, err := client.Get(serverURL + "/health")
 		if err == nil {
 			_ = resp.Body.Close()
 			if resp.StatusCode == http.StatusOK {
+				serverReady = true
 				break
 			}
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
+	require.True(t, serverReady, "server did not become ready at %s within expected time", serverURL)
internal/auditlog/store_postgresql.go (1)

104-131: Don’t swallow insert failures in small batches.

writeBatchSmall logs errors but always returns nil, masking partial data loss. Track failures and return a non‑nil error if any inserts fail.

🛠️ Proposed fix
 func (s *PostgreSQLStore) writeBatchSmall(ctx context.Context, entries []*LogEntry) error {
+	var failedCount int
 	for _, e := range entries {
 		var dataJSON []byte
 		if e.Data != nil {
 			var err error
@@
 		if err != nil {
 			slog.Warn("failed to insert audit log", "error", err, "id", e.ID)
+			failedCount++
 		}
 	}
+	if failedCount > 0 {
+		return fmt.Errorf("failed to insert %d of %d audit logs", failedCount, len(entries))
+	}
 	return nil
 }
internal/auditlog/middleware.go (2)

47-50: Health/metrics paths may still be logged when OnlyModelInteractions is disabled.

The OnlyModelInteractions filter doesn't cover operational paths like /health, /metrics, and /favicon.ico when disabled. The SkipLoggingPaths helper exists in stream_wrapper.go but isn't utilized here.

♻️ Proposed fix to always skip operational paths
 // Skip non-model paths if OnlyModelInteractions is enabled
 if cfg.OnlyModelInteractions && !IsModelInteractionPath(c.Request().URL.Path) {
 	return next(c)
 }
+
+// Always skip logging for health checks, metrics, and favicon
+if SkipLoggingPaths(c.Request().URL.Path) {
+	return next(c)
+}

178-189: Write error to capture buffer is discarded.

The error from r.body.Write(b) at line 181 is ignored. While this is a best-effort capture and unlikely to fail for an in-memory buffer, logging or propagating the error would aid debugging buffer issues.

♻️ Optional: Log write errors for debugging
 func (r *responseBodyCapture) Write(b []byte) (int, error) {
 	// Write to the capture buffer (limit to maxBodyCapture to avoid memory issues)
 	if r.body.Len() < int(maxBodyCapture) {
-		r.body.Write(b)
+		if _, err := r.body.Write(b); err != nil {
+			// Log at debug level - buffer writes rarely fail
+			// Consider: log.Debug().Err(err).Msg("failed to capture response body")
+		}
 		// Check if we just hit the limit
 		if r.body.Len() >= int(maxBodyCapture) {
 			r.truncated = true
 		}
 	}
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b55bab9 and d0e3314.

📒 Files selected for processing (7)
  • docker-compose.yaml
  • internal/auditlog/auditlog.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_sqlite.go
  • tests/e2e/auditlog_test.go
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/auditlog.go
  • tests/e2e/auditlog_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • tests/e2e/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/auditlog_test.go
🧠 Learnings (6)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/auditlog/middleware.go
🧬 Code graph analysis (5)
internal/auditlog/store_sqlite.go (1)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
internal/auditlog/store_postgresql.go (3)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
internal/core/types.go (1)
  • Model (56-61)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/auditlog/auditlog.go (2)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/server/http.go (1)
  • Config (25-32)
tests/e2e/auditlog_test.go (4)
internal/auditlog/auditlog.go (2)
  • LogEntry (28-56)
  • Config (130-152)
internal/server/http.go (3)
  • Config (25-32)
  • Server (19-22)
  • New (35-136)
internal/auditlog/logger.go (2)
  • Logger (13-20)
  • NewLogger (24-44)
internal/core/types.go (3)
  • ChatRequest (4-10)
  • Model (56-61)
  • Message (25-28)
internal/auditlog/store_mongodb.go (2)
internal/storage/storage.go (1)
  • New (82-93)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
🪛 ast-grep (0.40.5)
tests/e2e/auditlog_test.go

[warning] 111-111: "Detected a network listener listening on 0.0.0.0 or an empty string.
This could unexpectedly expose the server publicly as it binds to all
available interfaces. Instead, specify another IP address that is not
0.0.0.0 nor the empty string."
Context: net.Listen("tcp", ":0")
Note: [CWE-200] Exposure of Sensitive Information to an Unauthorized Actor [REFERENCES]
- https://owasp.org/Top10/A01_2021-Broken_Access_Control

(avoid-bind-to-all-interfaces-go)

🪛 Checkov (3.2.334)
docker-compose.yaml

[medium] 19-20: Basic Auth Credentials

(CKV_SECRET_4)

🔇 Additional comments (22)
internal/auditlog/store_sqlite.go (2)

22-84: Constructor and index setup look solid.


86-145: The SQLite variable limit concern does not apply to this codebase.

The review assumes SQLITE_MAX_VARIABLE_NUMBER = 999, but the codebase uses modernc.org/sqlite v1.44.0, which sets SQLITE_MAX_VARIABLE_NUMBER = 32766. With this limit and a logger batch size of 100 entries (100 × 16 columns = 1,600 variables), the batch insert is well within the safe threshold. No chunking is required.

Likely an incorrect or invalid review comment.

internal/auditlog/store_mongodb.go (2)

17-36: Error typing for partial writes is clean and helpful.


117-155: WriteBatch partial‑failure handling looks robust.

tests/e2e/auditlog_test.go (7)

25-105: Mock store is thread‑safe and well isolated for tests.


160-375: Core middleware coverage is comprehensive.


377-514: Streaming scenarios are well covered.


516-568: Concurrency test assertions look solid.


571-627: Header redaction matrix is thorough.


629-702: Error capture tests validate the critical paths.


704-863: OnlyModelInteractions coverage is strong.

internal/auditlog/store_postgresql.go (1)

22-87: Schema + index initialization is clear and consistent.

internal/auditlog/auditlog.go (2)

102-126: Redaction helper is efficient and clear.


154-164: Defaults are sensible for production safety.

internal/auditlog/middleware.go (8)

1-21: LGTM!

Import organization is clean and all dependencies are appropriate. Using Echo v4 as required by project conventions.


23-32: LGTM!

Good use of a custom contextKey type to prevent context key collisions. The maxBodyCapture constant properly addresses the previous feedback about magic numbers.


191-208: LGTM!

Proper implementation of http.Flusher and http.Hijacker interfaces to support SSE streaming and WebSocket upgrades. The delegation pattern correctly handles cases where the underlying writer doesn't support these interfaces.


210-220: LGTM!

Good consolidation of the duplicate header extraction functions from the previous review.


236-257: LGTM!

EnrichEntry now correctly accesses token fields directly on LogEntry rather than through entry.Data, resolving the previous nil-check concern. The nil/type guards are appropriate.


259-302: LGTM!

EnrichEntryWithError properly guards against nil entry.Data before assignment. The toValidUTF8String function is a good defensive measure for MongoDB BSON compatibility.


304-348: LGTM!

Robust decompression implementation with proper safeguards:

  • Compression bomb protection via maxDecompressedSize limit
  • Correct handling of different reader interfaces (gzip returns error, deflate/brotli don't)
  • Graceful fallback to original body on failure

The silent failure on decompression errors is appropriate for audit logging where partial data is better than nothing.


350-355: LGTM!

Clean, strongly-typed struct for token usage data following coding guidelines.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +92 to +99
// Parse JSON to interface{} for native BSON storage in MongoDB
var parsed interface{}
if jsonErr := json.Unmarshal(bodyBytes, &parsed); jsonErr == nil {
entry.Data.RequestBody = parsed
} else {
// Fallback: store as valid UTF-8 string if not valid JSON
entry.Data.RequestBody = toValidUTF8String(bodyBytes)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider defining typed structs for known payload formats.

Using interface{} for request/response bodies conflicts with the coding guideline requiring strongly-typed structs. While audit logging arbitrary JSON makes strict typing impractical, consider defining structs for known API payloads (e.g., OpenAI chat completion format) with a fallback to raw bytes/string for unknown formats.

♻️ Suggested approach
// Define known payload types
type ChatCompletionRequest struct {
    Model    string    `json:"model"`
    Messages []Message `json:"messages"`
    Stream   bool      `json:"stream,omitempty"`
    // ... other fields
}

// In middleware, try typed parsing first
var chatReq ChatCompletionRequest
if jsonErr := json.Unmarshal(bodyBytes, &chatReq); jsonErr == nil && chatReq.Model != "" {
    entry.Data.RequestBody = chatReq
} else {
    // Fallback for unknown formats
    entry.Data.RequestBody = toValidUTF8String(bodyBytes)
}

Based on coding guidelines requiring strongly-typed structs.

🤖 Prompt for AI Agents
In `@internal/auditlog/middleware.go` around lines 92 - 99, Replace the untyped
json.Unmarshal into interface{} with a typed-first approach: define
strongly-typed structs for known payloads (e.g., ChatCompletionRequest with
fields like Model and Messages), then in the middleware try
json.Unmarshal(bodyBytes, &chatReq) and validate a required field (e.g.,
chatReq.Model != "") before assigning entry.Data.RequestBody = chatReq; if that
fails fall back to the existing behavior (entry.Data.RequestBody =
toValidUTF8String(bodyBytes)). Keep json.Unmarshal and toValidUTF8String usage,
and ensure assignment targets entry.Data.RequestBody so existing downstream code
still works.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@internal/auditlog/logger.go`:
- Around line 144-159: The flushBatch method currently logs and drops batches
when l.store.WriteBatch fails; update Logger.flushBatch to perform a limited
retry loop with exponential backoff (e.g., 3 attempts with increasing sleep)
when l.store.WriteBatch returns an error, ensuring the ctx/timeout is managed
across retries (or recreate a fresh context per attempt) and include the attempt
number in logs; additionally, on final failure persist the failed batch to a
durable fallback (file/queue) or enqueue it in an in-memory retry queue for
later recovery and log that persistent storage action including error details
from WriteBatch and the fallback result.
- Around line 84-93: Set the Logger.closed atomic flag in Logger.Close() before
closing the l.done channel to prevent Write() from racing with flushLoop;
specifically, in the Close() method set l.closed.Store(true) (or equivalent)
prior to close(l.done), then wait (l.wg.Wait()) and return l.store.Close(); also
remove the redundant closed flag write inside flushLoop so only Close() controls
closed state and avoids Write() sending on a closed buffer.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0e3314 and 6e6aebc.

📒 Files selected for processing (1)
  • internal/auditlog/logger.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/logger.go
🧬 Code graph analysis (1)
internal/auditlog/logger.go (2)
internal/auditlog/auditlog.go (3)
  • LogStore (13-24)
  • Config (130-152)
  • LogEntry (28-56)
internal/server/http.go (1)
  • Config (25-32)
🔇 Additional comments (6)
internal/auditlog/logger.go (6)

11-22: LGTM!

The Logger struct is well-designed with strongly-typed fields, uses channels for async buffering, and includes the closed atomic flag for shutdown coordination. This aligns with the coding guidelines for high-throughput concurrency patterns.


24-46: LGTM!

Constructor properly initializes defaults, creates the buffered channel, and correctly tracks the goroutine lifecycle with WaitGroup.Add(1) before spawning flushLoop.


48-75: LGTM on the Write method logic.

The nil-entry check, closed-flag guard, and non-blocking send pattern are correctly implemented. The fallback to "unknown" for empty RequestID is a good defensive practice.

Note: The race condition between Write() and Close() is addressed in a separate comment on the Close() method.


95-142: LGTM on flushLoop structure.

The batch collection logic is correct. The slice reuse issue from past reviews is properly addressed by using make([]*LogEntry, 0, 100) for fresh slices. The shutdown sequence correctly drains the buffer and flushes with a reasonable timeout.


161-175: LGTM!

Clean null object pattern implementation. The NoopLogger correctly implements LoggerInterface with no-op behavior for when audit logging is disabled.


177-182: LGTM!

The LoggerInterface provides a clean abstraction enabling polymorphism between Logger and NoopLogger, facilitating easy testing and conditional logging in downstream components.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +84 to +93
func (l *Logger) Close() error {
// Signal the flush loop to stop
close(l.done)

// Wait for the flush loop to finish
l.wg.Wait()

// Close the store
return l.store.Close()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition: closed flag must be set in Close() before signaling shutdown.

The closed flag is currently set in flushLoop (line 123) after receiving on l.done. This creates a race window where Write() can check closed.Load() (returns false), then flushLoop sets closed=true and closes the buffer, and finally Write() panics attempting to send on the closed channel.

The fix should set closed in Close() before closing the done channel:

🐛 Proposed fix
 func (l *Logger) Close() error {
+	// Mark as closed first to prevent new writes
+	l.closed.Store(true)
+
 	// Signal the flush loop to stop
 	close(l.done)

 	// Wait for the flush loop to finish
 	l.wg.Wait()

 	// Close the store
 	return l.store.Close()
 }

And remove the redundant set in flushLoop:

 		case <-l.done:
-			// Shutdown: mark as closed before closing buffer to prevent Write() panics
-			l.closed.Store(true)
+			// Shutdown: closed flag already set by Close()
 			close(l.buffer)
🤖 Prompt for AI Agents
In `@internal/auditlog/logger.go` around lines 84 - 93, Set the Logger.closed
atomic flag in Logger.Close() before closing the l.done channel to prevent
Write() from racing with flushLoop; specifically, in the Close() method set
l.closed.Store(true) (or equivalent) prior to close(l.done), then wait
(l.wg.Wait()) and return l.store.Close(); also remove the redundant closed flag
write inside flushLoop so only Close() controls closed state and avoids Write()
sending on a closed buffer.

Comment on lines +144 to +159
// flushBatch writes a batch of entries to the store.
func (l *Logger) flushBatch(batch []*LogEntry) {
if len(batch) == 0 {
return
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := l.store.WriteBatch(ctx, batch); err != nil {
slog.Error("failed to write audit log batch",
"error", err,
"count", len(batch),
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding retry logic for transient write failures.

When WriteBatch fails, entries are lost with only an error log. For audit compliance scenarios, consider implementing a simple retry with backoff, or at minimum storing failed batches for later recovery.

♻️ Optional enhancement with simple retry
 func (l *Logger) flushBatch(batch []*LogEntry) {
 	if len(batch) == 0 {
 		return
 	}

 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer cancel()

-	if err := l.store.WriteBatch(ctx, batch); err != nil {
-		slog.Error("failed to write audit log batch",
-			"error", err,
-			"count", len(batch),
-		)
+	var lastErr error
+	for attempt := 0; attempt < 3; attempt++ {
+		if err := l.store.WriteBatch(ctx, batch); err != nil {
+			lastErr = err
+			time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
+			continue
+		}
+		return // Success
+	}
+	if lastErr != nil {
+		slog.Error("failed to write audit log batch after retries",
+			"error", lastErr,
+			"count", len(batch),
+		)
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// flushBatch writes a batch of entries to the store.
func (l *Logger) flushBatch(batch []*LogEntry) {
if len(batch) == 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := l.store.WriteBatch(ctx, batch); err != nil {
slog.Error("failed to write audit log batch",
"error", err,
"count", len(batch),
)
}
}
// flushBatch writes a batch of entries to the store.
func (l *Logger) flushBatch(batch []*LogEntry) {
if len(batch) == 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
if err := l.store.WriteBatch(ctx, batch); err != nil {
lastErr = err
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
continue
}
return // Success
}
if lastErr != nil {
slog.Error("failed to write audit log batch after retries",
"error", lastErr,
"count", len(batch),
)
}
}
🤖 Prompt for AI Agents
In `@internal/auditlog/logger.go` around lines 144 - 159, The flushBatch method
currently logs and drops batches when l.store.WriteBatch fails; update
Logger.flushBatch to perform a limited retry loop with exponential backoff
(e.g., 3 attempts with increasing sleep) when l.store.WriteBatch returns an
error, ensuring the ctx/timeout is managed across retries (or recreate a fresh
context per attempt) and include the attempt number in logs; additionally, on
final failure persist the failed batch to a durable fallback (file/queue) or
enqueue it in an in-memory retry queue for later recovery and log that
persistent storage action including error details from WriteBatch and the
fallback result.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@internal/auditlog/auditlog_test.go`:
- Around line 272-279: Replace the fragile time.Sleep(200 * time.Millisecond) in
the test with a deterministic polling helper on the mock store: implement a
method on mockStore named WaitForEntries(count int, timeout time.Duration) that
polls (under mockStore's mutex) until len(m.entries) >= count or timeout
expires, and return a bool; then update the test to call store.WaitForEntries(5,
500*time.Millisecond) and assert it returned true (e.g., require.True or
t.Fatalf on false) instead of using getEntries() immediately after sleeping.

In `@internal/auditlog/factory.go`:
- Around line 112-143: The storage.Storage methods returning interface{} should
be changed to return concrete types so createLogStore can avoid runtime asserts:
update the storage.Storage interface to expose PostgreSQLPool() *pgxpool.Pool
and MongoDatabase() *mongo.Database (and keep SQLiteDB() typed), refactor
package layout or use dependency injection to remove import cycles that
prevented these concrete returns, then simplify createLogStore to call
NewPostgreSQLStore(pgxPool, retentionDays) and NewMongoDBStore(mongoDB,
retentionDays) directly without type assertions (ensure nil checks remain and
adjust callers of PostgreSQLPool and MongoDatabase to match the new signatures).

In `@internal/auditlog/store_postgresql.go`:
- Around line 142-181: writeBatchLarge currently logs per-row insert failures
and still commits the transaction, causing inconsistent behavior with
writeBatchSmall; change it to track any insert error, stop processing further
entries, and return an aggregated error so the transaction is rolled back (defer
tx.Rollback already present) instead of committing partial data. Specifically,
in writeBatchLarge, replace the current per-entry slog.Warn behavior with
capturing the first (or aggregated) error from tx.Exec (and include context like
entry ID), break/return immediately (so Commit is not reached) and return
fmt.Errorf with wrapped errors similar to writeBatchSmall; ensure you still
json.Marshal e.Data and include its marshal error handling but propagate Exec
errors upstream so writeBatchLarge fails the transaction on any insert error.

In `@tests/e2e/auditlog_test.go`:
- Around line 233-245: Test currently asserts RequestBody is
map[string]interface{} via entry := entries[0] and entry.Data.RequestBody;
change it to decode/convert the stored request payload into the concrete request
type used by your API (the same struct used to marshal requests) instead of
asserting a generic map. Locate the use of store.WaitForAPIEntries and the test
block referencing entry.Data.RequestBody, unmarshal or type-assert the raw
payload into the concrete request struct (e.g., YourAPIRequest or OpenAIRequest)
and then assert typed fields like .Model == "gpt-4"; remove the
map[string]interface{} assertion.
♻️ Duplicate comments (1)
tests/e2e/auditlog_test.go (1)

107-158: Fail fast if the test server never becomes healthy.
Right now the loop exits silently on timeout, which can lead to confusing downstream failures.

🛠️ Proposed fix
-	client := &http.Client{Timeout: 2 * time.Second}
-	for i := 0; i < 30; i++ {
+	client := &http.Client{Timeout: 2 * time.Second}
+	serverReady := false
+	for i := 0; i < 30; i++ {
 		resp, err := client.Get(serverURL + "/health")
 		if err == nil {
 			_ = resp.Body.Close()
 			if resp.StatusCode == http.StatusOK {
-				break
+				serverReady = true
+				break
 			}
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
+	require.True(t, serverReady, "server did not become healthy within timeout")
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e6aebc and 8bfcb9e.

📒 Files selected for processing (8)
  • cmd/gomodel/main.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/factory.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_postgresql.go
  • internal/providers/init.go
  • tests/e2e/auditlog_test.go
  • tests/e2e/main_test.go
🧰 Additional context used
📓 Path-based instructions (5)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • tests/e2e/main_test.go
  • internal/auditlog/factory.go
  • internal/auditlog/middleware.go
  • cmd/gomodel/main.go
  • internal/providers/init.go
  • internal/auditlog/auditlog_test.go
  • tests/e2e/auditlog_test.go
  • internal/auditlog/store_postgresql.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • tests/e2e/main_test.go
  • internal/auditlog/auditlog_test.go
  • tests/e2e/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/main_test.go
  • tests/e2e/auditlog_test.go
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
internal/providers/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Streaming responses must return io.ReadCloser and callers are responsible for closing the stream

Files:

  • internal/providers/init.go
🧠 Learnings (14)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/main_test.go
  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • internal/auditlog/middleware.go
  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/{router,registry}.go : The ModelRegistry must be initialized before routing requests; check `ModelCount() > 0` or use router after `InitializeAsync()` completes to avoid `ErrRegistryNotInitialized` errors

Applied to files:

  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Applied to files:

  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/cache/**/*.go : Cache structure must use `ModelCache` struct with Version, UpdatedAt, and Models (map[string]CachedModel) fields

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Each provider package must include an `init()` function that registers itself with the factory using `providers.RegisterFactory("provider-name", NewProvider)`

Applied to files:

  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/router.go : When multiple providers support the same model, the first registered provider wins the routing decision

Applied to files:

  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : At least one provider API key is required in the environment configuration to start the server

Applied to files:

  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • cmd/gomodel/main.go
  • internal/providers/init.go
  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • internal/providers/init.go
  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • internal/auditlog/auditlog_test.go
  • tests/e2e/auditlog_test.go
🧬 Code graph analysis (3)
internal/auditlog/middleware.go (6)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/server/http.go (1)
  • Config (25-32)
internal/auditlog/stream_wrapper.go (2)
  • IsModelInteractionPath (251-263)
  • IsEntryMarkedAsStreaming (241-248)
internal/llmclient/client.go (2)
  • Request (178-183)
  • Response (186-189)
internal/core/types.go (2)
  • Usage (49-53)
  • Model (56-61)
internal/core/errors.go (1)
  • ErrorType (11-11)
cmd/gomodel/main.go (7)
internal/providers/init.go (1)
  • Init (64-66)
internal/auditlog/factory.go (1)
  • New (47-79)
internal/server/http.go (3)
  • New (35-136)
  • Config (25-32)
  • Server (19-22)
internal/storage/storage.go (2)
  • New (82-93)
  • Config (20-32)
internal/auditlog/auditlog.go (1)
  • Config (130-152)
config/config.go (1)
  • Config (26-33)
internal/providers/router.go (1)
  • Router (18-20)
internal/providers/init.go (5)
internal/providers/registry.go (2)
  • ModelRegistry (25-33)
  • NewModelRegistry (36-41)
internal/providers/router.go (2)
  • Router (18-20)
  • NewRouter (25-32)
internal/cache/redis.go (2)
  • DefaultRedisTTL (19-19)
  • NewRedisCache (43-76)
internal/cache/local.go (1)
  • NewLocalCache (21-25)
internal/providers/factory.go (1)
  • Create (41-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (47)
internal/auditlog/auditlog_test.go (8)

1-17: LGTM! Well-organized imports and package declaration.

The test file properly imports necessary packages including the brotli library for compression testing, and uses the sync package for thread-safe mock implementations.


19-113: Comprehensive header redaction test coverage.

The TestRedactHeaders function thoroughly covers edge cases including nil/empty headers, case-insensitive matching, and multiple sensitive header types. The table-driven test pattern is well-structured.


115-213: Good JSON marshaling test coverage.

TestLogEntryJSON and TestLogDataWithBodies properly verify round-trip JSON serialization and handle nested structures correctly. The use of map[string]interface{} for bodies aligns with the flexible storage requirements for different backends.


215-250: Thread-safe mockStore implementation.

The mockStore correctly implements mutex protection for concurrent access, addressing the race condition concern from previous reviews. The getEntries() and isClosed() helper methods properly acquire locks before reading shared state.


355-431: Thorough SSE usage parsing tests.

TestParseUsageFromSSE covers important edge cases including empty input, missing usage data, and correctly extracting usage from the last non-DONE event. The test data accurately reflects real SSE stream formats.


584-642: Good API key hashing test coverage.

The TestHashAPIKey function verifies empty inputs, consistency (same input produces same hash), and uniqueness (different inputs produce different hashes). The hash length assertion (16 characters) aligns with the updated apiKeyHashPrefixLength constant.


644-667: Helper compression functions correctly implemented.

The compressGzip, compressDeflate, and compressBrotli helpers properly use their respective compression libraries. Note that errors are silently ignored (_ =), which is acceptable for test helpers since compression of valid data shouldn't fail.


669-783: Comprehensive decompression test coverage.

TestDecompressBody, TestDecompressBodyInvalidData, and TestDecompressBodyEmptyInput thoroughly test all supported encodings (gzip, deflate, brotli), edge cases (unknown encoding, uppercase, multiple encodings), and error handling (invalid data, empty input, nil input).

internal/auditlog/store_postgresql.go (5)

15-21: Good struct design with sync.Once for safe close.

The PostgreSQLStore properly includes closeOnce sync.Once to prevent double-close panics, addressing the concern from previous reviews.


26-88: Well-structured initialization with proper index creation.

The NewPostgreSQLStore constructor properly validates the pool, creates the table with appropriate column types, and creates useful indexes. The GIN index on the JSONB data column enables efficient querying of nested JSON fields.


105-140: Improved error handling in writeBatchSmall.

The function now properly aggregates errors and returns a descriptive error message when inserts fail, addressing the previous review concern about silently succeeding on partial failures.


188-198: Safe Close() implementation with sync.Once.

The Close() method properly uses sync.Once to prevent double-close panics on the stopCleanup channel, and includes a nil check for safety.


200-239: Well-implemented cleanup loop with proper lifecycle management.

The cleanupLoop and cleanup functions properly handle the retention policy with appropriate timeout contexts and graceful shutdown via the stop channel.

internal/auditlog/factory.go (4)

15-40: Well-designed Result struct with proper resource cleanup.

The Result struct properly encapsulates the logger and storage, and the Close() method correctly aggregates errors from both resources. Safe to call multiple times.


42-79: Good initialization flow with proper cleanup on failure.

The New() function properly handles the disabled case, creates storage and log store in sequence, and ensures store.Close() is called if createLogStore fails.


81-110: Reasonable default values in buildStorageConfig.

The defaults (SQLite type, .cache/gomodel.db path, gomodel MongoDB database) are sensible for local development.


145-166: Good default handling in buildLoggerConfig.

The function properly applies defaults for BufferSize (1000) and FlushInterval (5 seconds) when values are zero or negative.

internal/providers/init.go (5)

16-37: Good InitResult design with safe cleanup.

Setting r.stopRefresh = nil after calling it prevents double-call issues. The Close() method properly handles nil cache.


39-51: Sensible default refresh interval.

The 5-minute default for RefreshInterval aligns with the learnings about provider model registration refreshing asynchronously every 5 minutes.


68-121: Well-structured initialization with proper cleanup on failure.

The InitWithConfig function properly cleans up resources (cache, stopRefresh) at each failure point. The non-blocking async initialization aligns with the codebase patterns.


132-165: Good cache backend selection with sensible defaults.

The initCache function properly defaults to "local" cache and handles Redis configuration with TTL defaults. The logging provides good observability.


167-194: Deterministic provider registration order.

Sorting provider names ensures consistent initialization order across runs, which aids debugging and reproducibility. The error handling correctly continues on individual provider failures while tracking the count.

internal/auditlog/middleware.go (9)

26-36: Well-defined constants for body capture limits and hash length.

The maxBodyCapture (1MB) and apiKeyHashPrefixLength (16 chars = 64 bits) constants address previous review concerns about memory exhaustion and collision risk respectively.


38-54: Proper early exits for disabled logging and non-model paths.

The middleware correctly skips processing when logging is disabled or when OnlyModelInteractions is enabled and the path is not a model interaction endpoint.


88-108: Request body capture with size limit protection.

The check for req.ContentLength > maxBodyCapture before reading prevents memory exhaustion attacks, addressing the previous review concern. The body is properly restored for downstream handlers.


163-166: Streaming check prevents duplicate logging.

The IsEntryMarkedAsStreaming(c) check ensures streaming requests are not logged twice (once by middleware, once by StreamLogWrapper), addressing the previous review concern.


173-212: responseBodyCapture properly implements Flusher and Hijacker.

The Flush() and Hijack() methods correctly delegate to the underlying ResponseWriter if it supports those interfaces, addressing the previous SSE streaming concern.


240-261: Missing nil check for entry.Data in EnrichEntry.

EnrichEntry sets token fields on entry directly (lines 257-259), not on entry.Data, so this isn't actually an issue. The fields PromptTokens, CompletionTokens, and TotalTokens are on LogEntry itself based on usage in the test file.


263-294: EnrichEntryWithError has appropriate nil check for entry.Data.

The function correctly checks entry.Data != nil before setting ErrorMessage, while ErrorType is set on the entry directly.


308-352: Robust decompression with bomb protection.

The decompressBody function includes a 2MB limit (maxDecompressedSize) to protect against compression bombs, and properly handles all supported encodings (gzip, deflate, brotli).


354-359: These are intentionally separate types serving different purposes and should not be consolidated.

The core.Usage struct in internal/core/types.go is part of the API response layer with JSON struct tags (json:"prompt_tokens", etc.) for serialization. The auditlog.Usage struct in internal/auditlog/middleware.go is an internal audit logging type without serialization tags. Consolidating them would unnecessarily couple the API response contract with internal middleware logic. They have the same field names but serve fundamentally different purposes.

Likely an incorrect or invalid review comment.

cmd/gomodel/main.go (6)

4-14: Imports align with the new startup/shutdown flow.
Clean and consistent with the added functionality.


75-81: Provider initialization lifecycle looks correct.
The Init + deferred Close pattern is solid.


92-109: Audit logging bootstrap and status logs look good.
Clear enable/disable logging with config details.


111-120: Server config wiring for audit logging is correct.
AuditLogger + LogOnlyModelInteractions are properly threaded through.


122-136: Graceful shutdown path is well-structured.
Bounded timeout and error reporting are appropriate.


141-147: Startup error handling correctly distinguishes ErrServerClosed.
Behavior matches the intended graceful shutdown semantics.

tests/e2e/main_test.go (2)

49-73: Loopback-only binding improves test isolation.
Good shift from localhost/any-interface to 127.0.0.1 for the URL and server bind.


122-125: Port discovery now binds to loopback only.
Consistent with the rest of the test networking changes.

tests/e2e/auditlog_test.go (8)

1-23: E2E build tag and imports look good.
File is correctly gated and dependencies are minimal.


25-105: mockLogStore helpers are thread-safe and clear.
Good use of mutex and copy semantics.


247-375: Header redaction, disabled logging, and API-key hashing coverage is solid.
These cases provide good behavioral guarantees.


377-514: Streaming logging coverage looks comprehensive.
Good assertions on status, headers, and duration.


516-569: Concurrency test validates count and uniqueness.
Nice coverage for concurrent logging behavior.


571-627: Header redaction matrix looks good.
Covers a useful spread of sensitive headers.


629-702: Error capture scenarios are well exercised.
Bad model + invalid JSON are both validated.


704-863: OnlyModelInteractions behavior is well covered.
Good separation between model and health endpoints.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +112 to +143
// createLogStore creates the appropriate LogStore for the given storage backend.
func createLogStore(store storage.Storage, retentionDays int) (LogStore, error) {
switch store.Type() {
case storage.TypeSQLite:
return NewSQLiteStore(store.SQLiteDB(), retentionDays)

case storage.TypePostgreSQL:
pool := store.PostgreSQLPool()
if pool == nil {
return nil, fmt.Errorf("PostgreSQL pool is nil")
}
pgxPool, ok := pool.(*pgxpool.Pool)
if !ok {
return nil, fmt.Errorf("invalid PostgreSQL pool type: %T", pool)
}
return NewPostgreSQLStore(pgxPool, retentionDays)

case storage.TypeMongoDB:
db := store.MongoDatabase()
if db == nil {
return nil, fmt.Errorf("MongoDB database is nil")
}
mongoDB, ok := db.(*mongo.Database)
if !ok {
return nil, fmt.Errorf("invalid MongoDB database type: %T", db)
}
return NewMongoDBStore(mongoDB, retentionDays)

default:
return nil, fmt.Errorf("unknown storage type: %s", store.Type())
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the storage package and interface definition
find . -type f -name "*.go" | head -20

Repository: ENTERPILOT/GOModel

Length of output: 645


🏁 Script executed:

# Search for the Storage interface definition
rg "type Storage interface" -A 20

Repository: ENTERPILOT/GOModel

Length of output: 1369


🏁 Script executed:

# Also search for the PostgreSQLPool and MongoDatabase methods
rg "PostgreSQLPool|MongoDatabase" -B 2 -A 2

Repository: ENTERPILOT/GOModel

Length of output: 2698


Refactor storage.Storage interface to use strongly-typed returns instead of interface{}.

The current interface violates the coding guideline requiring strongly-typed structs instead of interface{} to catch errors at compile time. While the comments note that interface{} is used to avoid import cycles, this is an architectural issue that should be resolved by:

  • Using proper dependency injection or
  • Reorganizing packages to eliminate circular dependencies
  • Defining the return types explicitly as *pgxpool.Pool and *mongo.Database

The type assertions in createLogStore are necessary only because of this guideline violation. Fixing the interface design would eliminate the need for runtime type assertions and align with the project's standards.

🤖 Prompt for AI Agents
In `@internal/auditlog/factory.go` around lines 112 - 143, The storage.Storage
methods returning interface{} should be changed to return concrete types so
createLogStore can avoid runtime asserts: update the storage.Storage interface
to expose PostgreSQLPool() *pgxpool.Pool and MongoDatabase() *mongo.Database
(and keep SQLiteDB() typed), refactor package layout or use dependency injection
to remove import cycles that prevented these concrete returns, then simplify
createLogStore to call NewPostgreSQLStore(pgxPool, retentionDays) and
NewMongoDBStore(mongoDB, retentionDays) directly without type assertions (ensure
nil checks remain and adjust callers of PostgreSQLPool and MongoDatabase to
match the new signatures).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strict typing guideline applies to request/response payloads (API types),
not internal infrastructure interfaces. Using interface{} here is a pragmatic
trade-off to avoid import cycles between storage and auditlog packages. The
type assertion happens once at initialization, not on every request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SantiagoDePolonia, thank you for the clarification! You're absolutely right—this is internal infrastructure code, not an API layer, and the type assertion at initialization time is a reasonable trade-off to avoid circular dependencies. I've noted the distinction for future reviews.


✏️ Learnings added
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/factory.go:112-143
Timestamp: 2026-01-15T23:07:37.652Z
Learning: The strict typing guideline requiring strongly-typed structs instead of interface{} or map[string]interface{} applies specifically to request/response payloads (API types), not to internal infrastructure interfaces where interface{} may be used pragmatically to avoid import cycles.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Comment on lines +142 to +181
// writeBatchLarge uses batch insert for larger batches
func (s *PostgreSQLStore) writeBatchLarge(ctx context.Context, entries []*LogEntry) error {
// For larger batches, use individual inserts in a transaction
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx) //nolint:errcheck

for _, e := range entries {
var dataJSON []byte
if e.Data != nil {
dataJSON, err = json.Marshal(e.Data)
if err != nil {
slog.Warn("failed to marshal log data", "error", err, "id", e.ID)
dataJSON = []byte("{}")
}
}

_, err = tx.Exec(ctx, `
INSERT INTO audit_logs (id, timestamp, duration_ns, model, provider, status_code,
request_id, client_ip, method, path, stream,
prompt_tokens, completion_tokens, total_tokens, error_type, data)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (id) DO NOTHING
`, e.ID, e.Timestamp, e.DurationNs, e.Model, e.Provider, e.StatusCode,
e.RequestID, e.ClientIP, e.Method, e.Path, e.Stream,
e.PromptTokens, e.CompletionTokens, e.TotalTokens, e.ErrorType, dataJSON)

if err != nil {
slog.Warn("failed to insert audit log in batch", "error", err, "id", e.ID)
}
}

if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}

return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent error handling between small and large batch paths.

writeBatchLarge logs warnings for individual insert failures but commits the transaction regardless, potentially persisting partial data. This differs from writeBatchSmall which returns an aggregated error. Consider consistent behavior.

🐛 Option A: Track failures and return error (consistent with writeBatchSmall)
 func (s *PostgreSQLStore) writeBatchLarge(ctx context.Context, entries []*LogEntry) error {
 	tx, err := s.pool.Begin(ctx)
 	if err != nil {
 		return fmt.Errorf("failed to begin transaction: %w", err)
 	}
 	defer tx.Rollback(ctx) //nolint:errcheck

+	var failedCount int
 	for _, e := range entries {
 		// ... marshal logic ...

 		if err != nil {
 			slog.Warn("failed to insert audit log in batch", "error", err, "id", e.ID)
+			failedCount++
 		}
 	}

+	if failedCount > 0 {
+		return fmt.Errorf("failed to insert %d of %d audit logs", failedCount, len(entries))
+	}
+
 	if err := tx.Commit(ctx); err != nil {
 		return fmt.Errorf("failed to commit transaction: %w", err)
 	}
 	return nil
 }
♻️ Option B: Accept partial success (update writeBatchSmall to match)

If partial success is acceptable for audit logs (best-effort logging), document this behavior and make writeBatchSmall also return nil on partial failures.

🤖 Prompt for AI Agents
In `@internal/auditlog/store_postgresql.go` around lines 142 - 181,
writeBatchLarge currently logs per-row insert failures and still commits the
transaction, causing inconsistent behavior with writeBatchSmall; change it to
track any insert error, stop processing further entries, and return an
aggregated error so the transaction is rolled back (defer tx.Rollback already
present) instead of committing partial data. Specifically, in writeBatchLarge,
replace the current per-entry slog.Warn behavior with capturing the first (or
aggregated) error from tx.Exec (and include context like entry ID), break/return
immediately (so Commit is not reached) and return fmt.Errorf with wrapped errors
similar to writeBatchSmall; ensure you still json.Marshal e.Data and include its
marshal error handling but propagate Exec errors upstream so writeBatchLarge
fails the transaction on any insert error.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docker-compose.yaml`:
- Around line 87-93: The Adminer service (adminer) is exposed on host port
"8081:8080" with no auth; add a short comment above the adminer block stating
this is for development only, then restrict its exposure by either binding the
published port to localhost (127.0.0.1) or moving the service into a
development-only profile (e.g., add profiles: [dev]) or a separate
docker-compose.override.yml so it is not brought up in production; ensure
depends_on: postgres and restart: unless-stopped remain unchanged and document
in README that adminer is development-only and must not be deployed to
production.
♻️ Duplicate comments (3)
docker-compose.yaml (3)

19-20: Hardcoded credentials remain unaddressed.

The hardcoded credentials in POSTGRES_URL and the postgres service environment continue to trip the static analysis warning (CKV_SECRET_4). This was previously flagged and should be addressed by sourcing credentials from environment variables.


26-27: Unsafe logging defaults remain unaddressed.

Enabling LOGGING_LOG_BODIES=true and LOGGING_LOG_HEADERS=true by default risks capturing credentials, tokens, and PII. This was previously flagged - consider defaulting to false with explicit opt-in.


73-85: MongoDB service lacks authentication credentials.

This was previously flagged and remains unaddressed. The service is exposed without authentication, which poses a security risk.

Additionally, verify that mongo:8 image exists - MongoDB 7 was the latest version as of early 2025.

What is the latest MongoDB version available on Docker Hub?
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bfcb9e and ff29aea.

📒 Files selected for processing (1)
  • docker-compose.yaml
🧰 Additional context used
🪛 Checkov (3.2.334)
docker-compose.yaml

[medium] 19-20: Basic Auth Credentials

(CKV_SECRET_4)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
docker-compose.yaml (3)

35-38: LGTM!

The dependency configuration correctly ensures both postgres and mongodb are healthy before starting gomodel, which aligns with the audit logging backend options.


112-113: LGTM!

Volume definitions correctly correspond to the new database services.


55-56: > Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +87 to +93
adminer:
image: adminer:5
ports:
- "8081:8080"
depends_on:
- postgres
restart: unless-stopped
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider restricting Adminer access or documenting its purpose.

Adminer exposes database administration capabilities on port 8081 without authentication. While acceptable for local development, consider:

  • Adding a comment clarifying this is for development only
  • Ensuring this service is not included in production deployments
🤖 Prompt for AI Agents
In `@docker-compose.yaml` around lines 87 - 93, The Adminer service (adminer) is
exposed on host port "8081:8080" with no auth; add a short comment above the
adminer block stating this is for development only, then restrict its exposure
by either binding the published port to localhost (127.0.0.1) or moving the
service into a development-only profile (e.g., add profiles: [dev]) or a
separate docker-compose.override.yml so it is not brought up in production;
ensure depends_on: postgres and restart: unless-stopped remain unchanged and
document in README that adminer is development-only and must not be deployed to
production.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/server/handlers.go`:
- Around line 31-39: The handler marks entries as streaming before executing
streamFn, causing failed streamFn errors to be skipped by logging; in
handleStreamingResponse, call streamFn() first and only after a successful,
non-nil stream set auditlog.MarkEntryAsStreaming(c, true) and
auditlog.EnrichEntryWithStream(c, true) so
StreamLogWrapper/IsEntryMarkedAsStreaming sees streaming only when a real stream
exists and handleError can log failures normally.
♻️ Duplicate comments (5)
internal/auditlog/auditlog.go (1)

78-87: Previous feedback on interface{} for body fields.

This was flagged in a prior review. Based on learnings, interface{} is acceptable here since this is internal infrastructure code, the MongoDB BSON serialization rationale is documented, and type assertions don't occur on every request path. The trade-off for queryable BSON documents vs. base64 binary is reasonable.

internal/auditlog/auditlog_test.go (1)

272-279: Sleep-based synchronization may be flaky under CI load.

While the time.Sleep(200ms) approach works, it's timing-dependent and could be flaky. Consider adding a WaitForEntries polling helper for more deterministic test behavior, as suggested in previous reviews.

internal/auditlog/stream_wrapper.go (1)

437-474: Shallow copy of pointer fields in CreateStreamEntry.

Temperature and MaxTokens pointer fields are copied directly, meaning both the original and copied entry share the same underlying values. While typically fine (these values shouldn't change after creation), for true isolation consider deep copying.

internal/auditlog/logger.go (2)

82-93: Race condition: closed flag should be set in Close() before signaling shutdown.

The closed flag is currently set in flushLoop (line 123) after receiving on l.done. This creates a race window where Write() can check closed.Load() (returns false), then flushLoop sets closed=true and closes the buffer, and Write() panics attempting to send on the closed channel.

🐛 Proposed fix
 func (l *Logger) Close() error {
+	// Mark as closed first to prevent new writes
+	l.closed.Store(true)
+
 	// Signal the flush loop to stop
 	close(l.done)

 	// Wait for the flush loop to finish
 	l.wg.Wait()

 	// Close the store
 	return l.store.Close()
 }

And in flushLoop, remove the redundant set:

 		case <-l.done:
-			// Shutdown: mark as closed before closing buffer to prevent Write() panics
-			l.closed.Store(true)
+			// Shutdown: closed flag already set by Close()
 			close(l.buffer)

144-159: Consider adding retry logic for transient write failures.

When WriteBatch fails, entries are lost with only an error log. For audit compliance scenarios, consider implementing simple retry with backoff. This is a recommended enhancement rather than a blocker.

♻️ Optional enhancement with simple retry
 func (l *Logger) flushBatch(batch []*LogEntry) {
 	if len(batch) == 0 {
 		return
 	}

 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 	defer cancel()

-	if err := l.store.WriteBatch(ctx, batch); err != nil {
-		slog.Error("failed to write audit log batch",
-			"error", err,
-			"count", len(batch),
-		)
+	var lastErr error
+	for attempt := 0; attempt < 3; attempt++ {
+		if err := l.store.WriteBatch(ctx, batch); err != nil {
+			lastErr = err
+			time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
+			continue
+		}
+		return // Success
+	}
+	if lastErr != nil {
+		slog.Error("failed to write audit log batch after retries",
+			"error", lastErr,
+			"count", len(batch),
+		)
 	}
 }
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff29aea and 26a1394.

📒 Files selected for processing (7)
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/constants.go
  • internal/auditlog/logger.go
  • internal/auditlog/middleware.go
  • internal/auditlog/stream_wrapper.go
  • internal/server/handlers.go
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/constants.go
  • internal/server/handlers.go
  • internal/auditlog/logger.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
internal/server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use Echo (v4) framework for HTTP server setup and middleware, not Chi or other frameworks

Files:

  • internal/server/handlers.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • internal/auditlog/auditlog_test.go
🧠 Learnings (9)
📚 Learning: 2026-01-15T23:07:26.646Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/auditlog_test.go:272-279
Timestamp: 2026-01-15T23:07:26.646Z
Learning: Go code guideline: Avoid using interface{} and map[string]interface{} for API request/response payload types. This should be restricted to public API surfaces and data payloads. It is acceptable to use interface{} in internal infrastructure packages (e.g., storage abstractions) only to break import cycles, and only when type assertions occur at initialization (not on every request) to minimize runtime type checks and improve readability. Apply this rule broadly to files under internal/ that deal with payload-like types, but not to internal components that are pure infrastructure or have runtime-per-request type assertions.

Applied to files:

  • internal/auditlog/constants.go
  • internal/server/handlers.go
  • internal/auditlog/logger.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
📚 Learning: 2026-01-15T23:07:37.652Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/factory.go:112-143
Timestamp: 2026-01-15T23:07:37.652Z
Learning: Guideline: Do not use interface{} or map[string]interface{} for API request/response payload types. Prefer strongly-typed structs for API payload definitions to improve type safety, serialization, and documentation. Allow interface{} only in internal infrastructure code paths where pragmatic flexibility is necessary (e.g., to avoid import cycles or to handle highly dynamic internal contracts). In internal/auditlog/factory.go and similar non-API implementation files, applying this restriction is optional and should be evaluated on a case-by-case basis based on whether the type remains internal and does not define API boundary shapes.

Applied to files:

  • internal/auditlog/constants.go
  • internal/server/handlers.go
  • internal/auditlog/logger.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/server/handlers.go
  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • internal/server/handlers.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : The responses converter in `internal/providers/responses_converter.go` must handle the OpenAI→Responses stream conversion for all providers

Applied to files:

  • internal/server/handlers.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • internal/auditlog/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • internal/auditlog/auditlog_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/stream_wrapper.go
  • internal/auditlog/middleware.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/auditlog/middleware.go
🧬 Code graph analysis (5)
internal/server/handlers.go (5)
internal/core/interfaces.go (1)
  • RoutableProvider (30-39)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/auditlog/stream_wrapper.go (4)
  • MarkEntryAsStreaming (506-508)
  • GetStreamEntryFromContext (490-502)
  • CreateStreamEntry (439-474)
  • WrapStreamForLogging (430-435)
internal/auditlog/middleware.go (2)
  • EnrichEntryWithStream (270-282)
  • EnrichEntry (230-249)
internal/core/types.go (1)
  • Model (56-61)
internal/auditlog/logger.go (3)
internal/auditlog/auditlog.go (3)
  • LogStore (13-24)
  • Config (130-152)
  • LogEntry (28-56)
internal/server/http.go (1)
  • Config (25-32)
internal/auditlog/constants.go (1)
  • BatchFlushThreshold (19-19)
internal/auditlog/auditlog_test.go (4)
internal/auditlog/auditlog.go (4)
  • RedactHeaders (113-127)
  • LogEntry (28-56)
  • LogData (61-87)
  • Config (130-152)
internal/auditlog/logger.go (1)
  • NewLogger (26-46)
internal/auditlog/stream_wrapper.go (4)
  • IsModelInteractionPath (521-533)
  • NewStreamLogWrapper (55-85)
  • WrapStreamForLogging (430-435)
  • CreateStreamEntry (439-474)
internal/auditlog/middleware.go (1)
  • Usage (343-347)
internal/auditlog/auditlog.go (2)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/server/http.go (1)
  • Config (25-32)
internal/auditlog/middleware.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/auditlog/auditlog.go (4)
  • Config (130-152)
  • LogEntry (28-56)
  • LogData (61-87)
  • RedactHeaders (113-127)
internal/auditlog/stream_wrapper.go (2)
  • IsModelInteractionPath (521-533)
  • IsEntryMarkedAsStreaming (511-518)
internal/auditlog/constants.go (3)
  • MaxBodyCapture (7-7)
  • LogEntryKey (31-31)
  • APIKeyHashPrefixLength (23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (38)
internal/auditlog/constants.go (1)

1-36: LGTM! Well-structured constants with good documentation.

The use of a custom contextKey type for context keys is idiomatic Go and prevents key collisions. The buffer sizes and limits are reasonable for the described use cases.

internal/auditlog/auditlog.go (4)

11-24: LGTM! Clean interface design for storage abstraction.

The LogStore interface is well-defined with clear method documentation. The contract for concurrent safety is properly documented.


26-56: Well-structured LogEntry with good field organization.

The separation of indexed core fields from flexible data fields is a solid design choice for efficient querying across different storage backends.


102-127: LGTM! Efficient O(1) header redaction.

The pre-built redactedHeadersSet provides O(1) lookup for sensitive headers. The case-insensitive comparison via strings.ToLower is correct for HTTP headers.


129-165: LGTM! Sensible default configuration.

The defaults are well-chosen: logging disabled by default (opt-in), reasonable buffer size and flush interval, and OnlyModelInteractions=true limits logging to AI endpoints by default.

internal/auditlog/logger.go (4)

14-22: LGTM! Well-structured Logger with proper concurrency primitives.

The use of atomic.Bool for the closed flag and sync.WaitGroup for coordinating shutdown is appropriate for concurrent access.


24-46: LGTM! Proper initialization with sensible defaults.

The constructor correctly validates configuration and falls back to sensible defaults. The goroutine lifecycle is properly tracked with WaitGroup.


95-142: LGTM! Batch handling and shutdown logic is correct.

The batch slice is properly replaced with a new slice after each flush (fixing the previous race condition). The drain logic on shutdown correctly processes remaining buffered entries before closing the store.


161-182: LGTM! Clean interface pattern for logger polymorphism.

The NoopLogger provides a clean way to disable logging without nil checks throughout the codebase. The LoggerInterface enables proper dependency injection.

internal/auditlog/auditlog_test.go (7)

19-113: LGTM! Comprehensive RedactHeaders test coverage.

The tests cover nil input, empty headers, no sensitive headers, single and multiple sensitive headers, and case-insensitive matching. Good edge case coverage.


115-213: LGTM! JSON marshaling tests verify round-trip correctness.

The tests properly verify that LogEntry and LogData structs serialize and deserialize correctly, including the interface{} body fields.


215-250: LGTM! Thread-safe mockStore implementation.

The mutex protection on all methods and the safe accessor helpers (getEntries, isClosed) properly address the previously flagged race conditions.


324-431: LGTM! Comprehensive path and SSE parsing tests.

Good coverage for IsModelInteractionPath with various path patterns and parseUsageFromSSE with different SSE event scenarios including edge cases.


433-511: LGTM! Stream wrapper tests verify integration correctly.

The tests properly verify usage capture from SSE streams, wrapper behavior with nil/disabled loggers, and the async write path.


513-582: LGTM! CreateStreamEntry tests verify field copying and isolation.

Good verification that fields are copied correctly, Stream is set to true, and header maps are truly copied (not shared references).


584-783: LGTM! Comprehensive API key hashing and decompression tests.

Good coverage for hashAPIKey including consistency checks, and thorough decompressBody tests covering all supported encodings (gzip, deflate, brotli) plus edge cases like invalid data and empty input.

internal/auditlog/stream_wrapper.go (10)

14-50: LGTM! Well-designed stream response builder and wrapper structures.

The streamResponseBuilder cleanly separates ChatCompletion and Responses API fields. The StreamLogWrapper properly implements io.ReadCloser with all necessary state for usage capture.


52-85: LGTM! Proper initialization with configurable body logging.

The constructor correctly determines whether to log bodies based on logger configuration and detects the API type from the path.


87-111: LGTM! Read correctly buffers and processes SSE data.

The error handling for buffer writes is properly implemented, and the rolling buffer truncation keeps memory bounded to SSEBufferSize.


156-168: Using map[string]interface{} for SSE event parsing.

Per coding guidelines and learnings, interface{} is acceptable in internal infrastructure code when it avoids import cycles or handles dynamic contracts. Since this parses responses from various providers with different structures, the map approach is pragmatic. The final Usage struct remains strongly-typed.


170-255: LGTM! SSE event parsing correctly handles both API formats.

The parseChatCompletionEvent and parseResponsesAPIEvent methods properly extract metadata and accumulate content while respecting the MaxContentCapture limit.


257-293: LGTM! Close correctly finalizes and logs the entry.

The close method is properly idempotent, calculates accurate duration, extracts usage data, and ensures the log entry is written before closing the underlying stream.


295-356: LGTM! Response builders correctly reconstruct API-specific formats.

The builders properly construct the expected response structures for both ChatCompletion and Responses API formats, suitable for storage in the interface{} response body field.


358-425: LGTM! Usage parsing correctly extracts token counts from SSE data.

The backward search through events is the right approach since usage appears in the final event. The validation that at least one token count is positive prevents returning empty usage objects.


520-533: LGTM! Model path detection is straightforward.

The hardcoded paths cover the current model endpoints. As noted in prior reviews, this could be made configurable, but the current implementation is functional for the defined use cases.


476-518: LGTM! Helper functions are well-implemented.

The copyMap function correctly creates a true copy, and the context helpers provide clean APIs for managing streaming state in Echo context.

internal/auditlog/middleware.go (8)

1-21: LGTM on imports and package structure.

The imports are well-organized and appropriate for the middleware functionality. Using Echo v4 aligns with the coding guidelines.


29-68: LGTM on middleware initialization and request capture.

The middleware correctly:

  • Skips disabled logging and non-model paths
  • Generates request ID if missing
  • Initializes LogEntry with proper metadata
  • Hashes API key for identification without exposure

76-96: LGTM on request body capture with size limit.

The size check at line 79 properly prevents memory exhaustion by setting RequestBodyTooBigToHandle for large requests instead of reading them into memory. Body is correctly restored for the handler at line 93.


151-157: LGTM on streaming check to prevent double logging.

The IsEntryMarkedAsStreaming check correctly ensures that streaming requests are only logged by StreamLogWrapper, addressing the previous concern about duplicate logging.


161-200: LGTM on response capture wrapper with SSE/WebSocket support.

The responseBodyCapture correctly implements:

  • http.Flusher delegation for SSE streaming
  • http.Hijacker delegation for WebSocket upgrades
  • Truncation tracking with proper flag setting

202-226: LGTM on header extraction and API key hashing.

The consolidated extractHeaders function and configurable hash prefix length address the previous review comments.


228-282: LGTM on enrichment functions.

The EnrichEntry function now correctly sets token fields directly on LogEntry (not entry.Data), and EnrichEntryWithError properly guards against nil entry.Data before accessing ErrorMessage.


296-340: LGTM on decompression with compression bomb protection.

The 2MB limit for decompression is appropriate since 1MB compressed data can legitimately expand beyond the capture limit. The function gracefully falls back to original bytes on any failure.

internal/server/handlers.go (4)

15-27: LGTM on Handler struct extension.

Clean dependency injection of the audit logger through the constructor.


41-68: LGTM on stream wrapping and SSE setup.

The stream wrapping logic correctly:

  • Creates a copy of the log entry for streaming
  • Sets appropriate SSE headers before WriteHeader
  • Captures headers in the stream entry for auditing
  • Uses defer to ensure stream cleanup

81-98: LGTM on ChatCompletion audit enrichment and flow.

The handler correctly enriches audit logs before branching into streaming/non-streaming paths. Usage is nil at this point since it's only available after the response.


130-147: LGTM on Responses handler integration.

Consistent audit log enrichment and streaming delegation pattern with ChatCompletion, addressing the previous concern about duplicate streaming logic.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/server/handlers.go (3)

142-148: Same token usage logging gap as ChatCompletion.

Non-streaming Responses also doesn't capture token usage in the audit log. Consider applying the same fix as suggested for ChatCompletion if the response type includes usage data.


158-165: Consider using a typed struct for the fallback error response.

While the retrieved learnings indicate interface{} is acceptable in internal infrastructure, using a typed struct here would improve consistency with the coding guidelines and make the error format explicit.

♻️ Optional: Define typed error response
// ErrorResponse represents the standard error response format
type ErrorResponse struct {
	Error ErrorDetail `json:"error"`
}

type ErrorDetail struct {
	Type    string `json:"type"`
	Message string `json:"message"`
}

// Then in handleError:
return c.JSON(http.StatusInternalServerError, ErrorResponse{
	Error: ErrorDetail{
		Type:    "internal_error",
		Message: "an unexpected error occurred",
	},
})

93-99: Token usage from non-streaming responses is not logged, and the proposed fix contains a syntax error.

The EnrichEntry call at line 84 passes nil for usage. For non-streaming responses, resp.Usage contains token data but is not captured in the audit log.

Critical issue: The proposed fix attempts to nil-check resp.Usage using if resp.Usage != nil, but ChatResponse.Usage is a value type (not a pointer), so this code will not compile. Additionally, the same issue exists in the Responses handler (line ~125).

♻️ Corrected fix for ChatCompletion handler
 	// Non-streaming
 	resp, err := h.provider.ChatCompletion(c.Request().Context(), &req)
 	if err != nil {
 		return handleError(c, err)
 	}

+	// Enrich audit log with token usage from response
+	auditlog.EnrichEntry(c, req.Model, h.provider.GetProviderType(req.Model), &auditlog.Usage{
+		PromptTokens:     resp.Usage.PromptTokens,
+		CompletionTokens: resp.Usage.CompletionTokens,
+		TotalTokens:      resp.Usage.TotalTokens,
+	})
+
 	return c.JSON(http.StatusOK, resp)

Apply the same fix to the Responses handler (which has a pointer-type Usage field, so include a nil-check there).

🤖 Fix all issues with AI agents
In `@internal/auditlog/store_postgresql.go`:
- Around line 26-75: The NewPostgreSQLStore function uses context.Background()
for DDL and index creation which can hang; change it to use a bounded context
(e.g., ctx, cancel := context.WithTimeout(context.Background(),
<reasonableDuration>); defer cancel()) and pass that ctx into all pool.Exec
calls (table creation and the index loop), or refactor NewPostgreSQLStore to
accept a caller-provided context parameter and use that instead; ensure you also
import time if you choose a literal timeout and keep the defer cancel() so
resources are released.
- Around line 142-176: The writeBatchLarge function currently executes one
tx.Exec per entry causing many round-trips; replace the per-row Exec loop with a
pgx.Batch: create a pgx.Batch, for each entry Queue the INSERT (using the same
parameter order and dataJSON preparation in writeBatchLarge), call
tx.SendBatch(ctx) to send queued statements, iterate over the batch results to
check for errors, and then Close/Results.Close() and Commit the transaction;
ensure you still handle json.Marshal failures the same way and aggregate/report
any insert errors similarly to the current errs slice.

In `@internal/server/handlers.go`:
- Around line 67-68: Capture and handle the error returned by io.Copy instead of
discarding it: replace "_, _ = io.Copy(c.Response().Writer, wrappedStream)" with
code that assigns n, err := io.Copy(c.Response().Writer, wrappedStream) and, if
err != nil, logs the error (at debug/info level) along with context (e.g.,
client info or n) using your existing logger; keep the
c.Response().WriteHeader(http.StatusOK) call as-is and only treat
client-disconnect-like errors as non-fatal while emitting the log for
observability.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26a1394 and 073a553.

📒 Files selected for processing (2)
  • internal/auditlog/store_postgresql.go
  • internal/server/handlers.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/store_postgresql.go
  • internal/server/handlers.go
internal/server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use Echo (v4) framework for HTTP server setup and middleware, not Chi or other frameworks

Files:

  • internal/server/handlers.go
🧠 Learnings (5)
📚 Learning: 2026-01-15T23:07:26.646Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/auditlog_test.go:272-279
Timestamp: 2026-01-15T23:07:26.646Z
Learning: Go code guideline: Avoid using interface{} and map[string]interface{} for API request/response payload types. This should be restricted to public API surfaces and data payloads. It is acceptable to use interface{} in internal infrastructure packages (e.g., storage abstractions) only to break import cycles, and only when type assertions occur at initialization (not on every request) to minimize runtime type checks and improve readability. Apply this rule broadly to files under internal/ that deal with payload-like types, but not to internal components that are pure infrastructure or have runtime-per-request type assertions.

Applied to files:

  • internal/auditlog/store_postgresql.go
  • internal/server/handlers.go
📚 Learning: 2026-01-15T23:07:37.652Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/factory.go:112-143
Timestamp: 2026-01-15T23:07:37.652Z
Learning: Guideline: Do not use interface{} or map[string]interface{} for API request/response payload types. Prefer strongly-typed structs for API payload definitions to improve type safety, serialization, and documentation. Allow interface{} only in internal infrastructure code paths where pragmatic flexibility is necessary (e.g., to avoid import cycles or to handle highly dynamic internal contracts). In internal/auditlog/factory.go and similar non-API implementation files, applying this restriction is optional and should be evaluated on a case-by-case basis based on whether the type remains internal and does not define API boundary shapes.

Applied to files:

  • internal/auditlog/store_postgresql.go
  • internal/server/handlers.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/server/handlers.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • internal/server/handlers.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : The responses converter in `internal/providers/responses_converter.go` must handle the OpenAI→Responses stream conversion for all providers

Applied to files:

  • internal/server/handlers.go
🧬 Code graph analysis (2)
internal/auditlog/store_postgresql.go (2)
internal/auditlog/auditlog.go (1)
  • LogEntry (28-56)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/server/handlers.go (4)
internal/core/interfaces.go (1)
  • RoutableProvider (30-39)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/auditlog/stream_wrapper.go (4)
  • MarkEntryAsStreaming (506-508)
  • GetStreamEntryFromContext (490-502)
  • CreateStreamEntry (439-474)
  • WrapStreamForLogging (430-435)
internal/auditlog/middleware.go (2)
  • EnrichEntryWithStream (270-282)
  • EnrichEntry (230-249)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (11)
internal/server/handlers.go (4)

15-19: LGTM!

The Handler struct correctly integrates the audit logger with proper typing. Using auditlog.LoggerInterface instead of interface{} aligns with the coding guidelines for strongly-typed dependencies.


21-27: LGTM!

The constructor properly initializes the handler with both the provider and logger dependencies. The WrapStreamForLogging function handles nil loggers gracefully, so no nil check is needed here.


102-105: LGTM!

Simple health check endpoint with appropriate response.


107-115: LGTM!

The ListModels handler correctly delegates to the provider and returns the response. No model-specific audit enrichment is needed here since this is a discovery endpoint.

internal/auditlog/store_postgresql.go (7)

15-21: Nice: idempotent Close via sync.Once.

This prevents double-close panics and keeps shutdown safe.


90-103: LGTM: small vs large batch routing.

Clear split and early return on empty batch.


191-194: Flush no-op is appropriate for synchronous writes.


196-205: Close is safe and idempotent.

Good use of the stop channel + sync.Once.


208-225: Cleanup loop structure looks solid.

Ticker + stop channel is clear and predictable.


227-247: Cleanup is well-bounded and logs outcomes.

Timeout context and non-fatal error handling are appropriate.


105-138: No action needed. The code uses errors.Join, which is supported in Go 1.20+. This repository requires Go 1.24.0 (declared in go.mod), so there is no compatibility issue.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +26 to +75
func NewPostgreSQLStore(pool *pgxpool.Pool, retentionDays int) (*PostgreSQLStore, error) {
if pool == nil {
return nil, fmt.Errorf("connection pool is required")
}

ctx := context.Background()

// Create table with commonly-filtered fields as columns
_, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
duration_ns BIGINT DEFAULT 0,
model TEXT,
provider TEXT,
status_code INTEGER DEFAULT 0,
request_id TEXT,
client_ip TEXT,
method TEXT,
path TEXT,
stream BOOLEAN DEFAULT FALSE,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
error_type TEXT,
data JSONB
)
`)
if err != nil {
return nil, fmt.Errorf("failed to create audit_logs table: %w", err)
}

// Create indexes for common queries
indexes := []string{
"CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_logs(timestamp)",
"CREATE INDEX IF NOT EXISTS idx_audit_model ON audit_logs(model)",
"CREATE INDEX IF NOT EXISTS idx_audit_status ON audit_logs(status_code)",
"CREATE INDEX IF NOT EXISTS idx_audit_provider ON audit_logs(provider)",
"CREATE INDEX IF NOT EXISTS idx_audit_request_id ON audit_logs(request_id)",
"CREATE INDEX IF NOT EXISTS idx_audit_client_ip ON audit_logs(client_ip)",
"CREATE INDEX IF NOT EXISTS idx_audit_path ON audit_logs(path)",
"CREATE INDEX IF NOT EXISTS idx_audit_error_type ON audit_logs(error_type)",
"CREATE INDEX IF NOT EXISTS idx_audit_data_gin ON audit_logs USING GIN (data)",
}
for _, idx := range indexes {
if _, err := pool.Exec(ctx, idx); err != nil {
slog.Warn("failed to create index", "error", err)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add a timeout around DDL/index creation to avoid startup hangs.

context.Background() can block indefinitely if the DB is slow or misconfigured. Consider a bounded context (or accept a caller-provided context).

♻️ Proposed refactor (bounded startup context)
-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewPostgreSQLStore(pool *pgxpool.Pool, retentionDays int) (*PostgreSQLStore, error) {
if pool == nil {
return nil, fmt.Errorf("connection pool is required")
}
ctx := context.Background()
// Create table with commonly-filtered fields as columns
_, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
duration_ns BIGINT DEFAULT 0,
model TEXT,
provider TEXT,
status_code INTEGER DEFAULT 0,
request_id TEXT,
client_ip TEXT,
method TEXT,
path TEXT,
stream BOOLEAN DEFAULT FALSE,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
error_type TEXT,
data JSONB
)
`)
if err != nil {
return nil, fmt.Errorf("failed to create audit_logs table: %w", err)
}
// Create indexes for common queries
indexes := []string{
"CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_logs(timestamp)",
"CREATE INDEX IF NOT EXISTS idx_audit_model ON audit_logs(model)",
"CREATE INDEX IF NOT EXISTS idx_audit_status ON audit_logs(status_code)",
"CREATE INDEX IF NOT EXISTS idx_audit_provider ON audit_logs(provider)",
"CREATE INDEX IF NOT EXISTS idx_audit_request_id ON audit_logs(request_id)",
"CREATE INDEX IF NOT EXISTS idx_audit_client_ip ON audit_logs(client_ip)",
"CREATE INDEX IF NOT EXISTS idx_audit_path ON audit_logs(path)",
"CREATE INDEX IF NOT EXISTS idx_audit_error_type ON audit_logs(error_type)",
"CREATE INDEX IF NOT EXISTS idx_audit_data_gin ON audit_logs USING GIN (data)",
}
for _, idx := range indexes {
if _, err := pool.Exec(ctx, idx); err != nil {
slog.Warn("failed to create index", "error", err)
}
}
func NewPostgreSQLStore(pool *pgxpool.Pool, retentionDays int) (*PostgreSQLStore, error) {
if pool == nil {
return nil, fmt.Errorf("connection pool is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Create table with commonly-filtered fields as columns
_, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
duration_ns BIGINT DEFAULT 0,
model TEXT,
provider TEXT,
status_code INTEGER DEFAULT 0,
request_id TEXT,
client_ip TEXT,
method TEXT,
path TEXT,
stream BOOLEAN DEFAULT FALSE,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
error_type TEXT,
data JSONB
)
`)
if err != nil {
return nil, fmt.Errorf("failed to create audit_logs table: %w", err)
}
// Create indexes for common queries
indexes := []string{
"CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_logs(timestamp)",
"CREATE INDEX IF NOT EXISTS idx_audit_model ON audit_logs(model)",
"CREATE INDEX IF NOT EXISTS idx_audit_status ON audit_logs(status_code)",
"CREATE INDEX IF NOT EXISTS idx_audit_provider ON audit_logs(provider)",
"CREATE INDEX IF NOT EXISTS idx_audit_request_id ON audit_logs(request_id)",
"CREATE INDEX IF NOT EXISTS idx_audit_client_ip ON audit_logs(client_ip)",
"CREATE INDEX IF NOT EXISTS idx_audit_path ON audit_logs(path)",
"CREATE INDEX IF NOT EXISTS idx_audit_error_type ON audit_logs(error_type)",
"CREATE INDEX IF NOT EXISTS idx_audit_data_gin ON audit_logs USING GIN (data)",
}
for _, idx := range indexes {
if _, err := pool.Exec(ctx, idx); err != nil {
slog.Warn("failed to create index", "error", err)
}
}
🤖 Prompt for AI Agents
In `@internal/auditlog/store_postgresql.go` around lines 26 - 75, The
NewPostgreSQLStore function uses context.Background() for DDL and index creation
which can hang; change it to use a bounded context (e.g., ctx, cancel :=
context.WithTimeout(context.Background(), <reasonableDuration>); defer cancel())
and pass that ctx into all pool.Exec calls (table creation and the index loop),
or refactor NewPostgreSQLStore to accept a caller-provided context parameter and
use that instead; ensure you also import time if you choose a literal timeout
and keep the defer cancel() so resources are released.

Comment on lines +142 to +176
// writeBatchLarge uses batch insert for larger batches
func (s *PostgreSQLStore) writeBatchLarge(ctx context.Context, entries []*LogEntry) error {
// For larger batches, use individual inserts in a transaction
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx) //nolint:errcheck

var errs []error

for _, e := range entries {
var dataJSON []byte
if e.Data != nil {
dataJSON, err = json.Marshal(e.Data)
if err != nil {
slog.Warn("failed to marshal log data", "error", err, "id", e.ID)
dataJSON = []byte("{}")
}
}

_, err = tx.Exec(ctx, `
INSERT INTO audit_logs (id, timestamp, duration_ns, model, provider, status_code,
request_id, client_ip, method, path, stream,
prompt_tokens, completion_tokens, total_tokens, error_type, data)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (id) DO NOTHING
`, e.ID, e.Timestamp, e.DurationNs, e.Model, e.Provider, e.StatusCode,
e.RequestID, e.ClientIP, e.Method, e.Path, e.Stream,
e.PromptTokens, e.CompletionTokens, e.TotalTokens, e.ErrorType, dataJSON)

if err != nil {
slog.Warn("failed to insert audit log in batch", "error", err, "id", e.ID)
errs = append(errs, fmt.Errorf("insert %s: %w", e.ID, err))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check pgx version and examine the batch implementation
set -euo pipefail

# Find and display go.mod
echo "=== go.mod (pgx version) ==="
fd -a -t f 'go\.mod' | head -1 | xargs cat | grep -i pgx || echo "pgx not found directly; checking for indirect dependencies"

# Also check for indirect pgx references
fd -a -t f 'go\.mod' | head -1 | xargs cat

Repository: ENTERPILOT/GOModel

Length of output: 3004


🏁 Script executed:

#!/bin/bash
# Examine the store_postgresql.go file for context
set -euo pipefail

echo "=== File: internal/auditlog/store_postgresql.go ==="
fd -a -t f 'store_postgresql.go' -path '*/auditlog/*' | head -1 | xargs wc -l

# Get the full file to understand the structure
echo ""
echo "=== Full file content ==="
fd -a -t f 'store_postgresql.go' -path '*/auditlog/*' | head -1 | xargs cat

Repository: ENTERPILOT/GOModel

Length of output: 301


🏁 Script executed:

#!/bin/bash
# Search for batch configuration and expected batch sizes
set -euo pipefail

echo "=== Searching for batch-related configuration ==="
rg -i 'batch.*size|writebatch|batchsize' --type go || echo "No batch size config found"

echo ""
echo "=== Checking function calls to writeBatchLarge ==="
rg -n 'writeBatchLarge' --type go -B 2 -A 2 || echo "No calls found"

Repository: ENTERPILOT/GOModel

Length of output: 2945


🏁 Script executed:

#!/bin/bash
# Find and display store_postgresql.go
set -euo pipefail

echo "=== store_postgresql.go ==="
fd 'store_postgresql\.go' -type f | xargs cat

echo ""
echo "=== constants.go for batch size ==="
fd 'constants\.go' -type f -path '*/auditlog/*' | xargs cat 2>/dev/null || echo "Not found at auditlog path"

Repository: ENTERPILOT/GOModel

Length of output: 277


🏁 Script executed:

#!/bin/bash
# Check WriteBatch method and batch size threshold
set -euo pipefail

echo "=== WriteBatch method implementation ==="
rg -n 'func \(s \*PostgreSQLStore\) WriteBatch' -A 20 --type go

echo ""
echo "=== Batch size constant ==="
rg -n 'const.*[Bb]atch' --type go

Repository: ENTERPILOT/GOModel

Length of output: 1615


🏁 Script executed:

#!/bin/bash
# Find and read store_postgresql.go without --type flag
set -euo pipefail

echo "=== store_postgresql.go ==="
fd 'store_postgresql\.go' | head -1 | xargs cat

echo ""
echo "=== constants.go in auditlog ==="
fd 'constants\.go' -path '*/auditlog/*' | head -1 | xargs cat 2>/dev/null || echo "constants.go not found"

Repository: ENTERPILOT/GOModel

Length of output: 7605


🏁 Script executed:

#!/bin/bash
# Check if there are other references to pgx Batch or CopyFrom in the codebase
set -euo pipefail

echo "=== Looking for existing Batch/CopyFrom usage ==="
rg 'Batch|CopyFrom' --glob '*.go' || echo "No Batch or CopyFrom usage found"

echo ""
echo "=== Check pgx documentation capabilities in v5.8.0 ==="
rg 'github.com/jackc/pgx' | head -5

Repository: ENTERPILOT/GOModel

Length of output: 3226


Use pgx Batch for more efficient large-batch inserts.

Per-row Exec in a transaction still incurs multiple round-trips. For batches up to 100 entries, pgx Batch can queue all inserts and send them in fewer round-trips, significantly reducing latency and database load.

Example approach
batch := &pgx.Batch{}
for _, e := range entries {
  // ... prepare dataJSON ...
  batch.Queue(`INSERT INTO audit_logs (...) VALUES ($1, ..., $16) ON CONFLICT (id) DO NOTHING`,
    e.ID, e.Timestamp, ..., dataJSON)
}
results := tx.SendBatch(ctx, batch)
// Handle results...
results.Close()
🤖 Prompt for AI Agents
In `@internal/auditlog/store_postgresql.go` around lines 142 - 176, The
writeBatchLarge function currently executes one tx.Exec per entry causing many
round-trips; replace the per-row Exec loop with a pgx.Batch: create a pgx.Batch,
for each entry Queue the INSERT (using the same parameter order and dataJSON
preparation in writeBatchLarge), call tx.SendBatch(ctx) to send queued
statements, iterate over the batch results to check for errors, and then
Close/Results.Close() and Commit the transaction; ensure you still handle
json.Marshal failures the same way and aggregate/report any insert errors
similarly to the current errs slice.

Comment on lines +67 to +68
c.Response().WriteHeader(http.StatusOK)
_, _ = io.Copy(c.Response().Writer, wrappedStream)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider logging io.Copy errors for observability.

The error from io.Copy is silently discarded. While SSE streams may terminate unexpectedly (client disconnect), logging these errors at debug/info level would help diagnose connection issues.

♻️ Proposed improvement
 	c.Response().WriteHeader(http.StatusOK)
-	_, _ = io.Copy(c.Response().Writer, wrappedStream)
+	if _, err := io.Copy(c.Response().Writer, wrappedStream); err != nil {
+		// Log at debug level - client disconnects are common for SSE
+		c.Logger().Debug("stream copy ended", "error", err)
+	}
 	return nil
🤖 Prompt for AI Agents
In `@internal/server/handlers.go` around lines 67 - 68, Capture and handle the
error returned by io.Copy instead of discarding it: replace "_, _ =
io.Copy(c.Response().Writer, wrappedStream)" with code that assigns n, err :=
io.Copy(c.Response().Writer, wrappedStream) and, if err != nil, logs the error
(at debug/info level) along with context (e.g., client info or n) using your
existing logger; keep the c.Response().WriteHeader(http.StatusOK) call as-is and
only treat client-disconnect-like errors as non-fatal while emitting the log for
observability.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.claude/settings.local.json:
- Around line 8-14: The permissions list currently grants overly broad abilities
via the "WebSearch" and "Bash(grep:*)" entries; update the settings by removing
or relocating "WebSearch" and "Bash(grep:*)" from the main allowed list and
placing them under the "ask" (or equivalent permission-prompt) category, or
narrow them (e.g., replace "WebSearch" with a domain-limited WebFetch entry and
restrict "Bash(grep:*)" to specific patterns) so that only the documented tools
like "WebFetch(domain:docs.x.ai)", "Bash(go build:*)", "Bash(golangci-lint
run:*)", "Bash(make test-e2e:*)", and "Bash(go doc:*)" remain unrestricted.

In `@internal/auditlog/store_sqlite.go`:
- Around line 85-136: The WriteBatch implementation in SQLiteStore should
preserve NULL for missing data and be robust to SQLite parameter limits: when
marshalLogData(e.Data, e.ID) returns nil, append nil to the values slice instead
of string(dataJSON) so the data column is stored as SQL NULL (change where
values are appended in WriteBatch for the data field); additionally, split
entries into smaller chunks (e.g., ensure total bound parameters per ExecContext
call stays below a safe limit) and execute the INSERT OR IGNORE with
placeholders/values per chunk rather than assuming all entries fit in a single
query (adjust the placeholders construction and ExecContext loop in
SQLiteStore.WriteBatch accordingly).
♻️ Duplicate comments (3)
internal/auditlog/store_sqlite.go (1)

155-172: Add a timeout context for cleanup deletes.

Long-running deletes can block indefinitely; align with the bounded cleanup behavior in the PostgreSQL store.

♻️ Proposed fix
 func (s *SQLiteStore) cleanup() {
 	if s.retentionDays <= 0 {
 		return
 	}
 
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+	defer cancel()
+
 	cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)
 
-	result, err := s.db.Exec("DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
+	result, err := s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
internal/auditlog/store_postgresql.go (1)

25-73: Bound DDL/index creation with a timeout to avoid startup hangs.

Using context.Background() can block indefinitely on DB issues; use a bounded context or accept a caller-provided context.

♻️ Proposed fix
-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
internal/auditlog/auditlog.go (1)

60-89: Replace interface{} bodies with typed payload wrappers.

RequestBody/ResponseBody as interface{} breaks the strongly-typed payload requirement and weakens compile-time checks. Use a typed wrapper (e.g., LogBody with json.RawMessage) or explicit variants and update call sites accordingly. As per coding guidelines, payloads should be strongly typed.

🐛 Proposed fix (typed wrapper)
 type LogData struct {
 	...
-	RequestBody  interface{} `json:"request_body,omitempty" bson:"request_body,omitempty"`
-	ResponseBody interface{} `json:"response_body,omitempty" bson:"response_body,omitempty"`
+	RequestBody  *LogBody `json:"request_body,omitempty" bson:"request_body,omitempty"`
+	ResponseBody *LogBody `json:"response_body,omitempty" bson:"response_body,omitempty"`
 	...
 }
+
+// LogBody wraps raw JSON to keep payloads typed without using interface{}.
+type LogBody struct {
+	JSON json.RawMessage `json:"json,omitempty" bson:"json,omitempty"`
+}
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 073a553 and 82c5db7.

📒 Files selected for processing (5)
  • .claude/settings.local.json
  • internal/auditlog/auditlog.go
  • internal/auditlog/cleanup.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_sqlite.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • internal/auditlog/cleanup.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/auditlog.go
🧠 Learnings (4)
📚 Learning: 2026-01-15T23:07:26.646Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/auditlog_test.go:272-279
Timestamp: 2026-01-15T23:07:26.646Z
Learning: Go code guideline: Avoid using interface{} and map[string]interface{} for API request/response payload types. This should be restricted to public API surfaces and data payloads. It is acceptable to use interface{} in internal infrastructure packages (e.g., storage abstractions) only to break import cycles, and only when type assertions occur at initialization (not on every request) to minimize runtime type checks and improve readability. Apply this rule broadly to files under internal/ that deal with payload-like types, but not to internal components that are pure infrastructure or have runtime-per-request type assertions.

Applied to files:

  • internal/auditlog/cleanup.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/auditlog.go
📚 Learning: 2026-01-15T23:07:37.652Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/factory.go:112-143
Timestamp: 2026-01-15T23:07:37.652Z
Learning: Guideline: Do not use interface{} or map[string]interface{} for API request/response payload types. Prefer strongly-typed structs for API payload definitions to improve type safety, serialization, and documentation. Allow interface{} only in internal infrastructure code paths where pragmatic flexibility is necessary (e.g., to avoid import cycles or to handle highly dynamic internal contracts). In internal/auditlog/factory.go and similar non-API implementation files, applying this restriction is optional and should be evaluated on a case-by-case basis based on whether the type remains internal and does not define API boundary shapes.

Applied to files:

  • internal/auditlog/cleanup.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/auditlog.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles

Applied to files:

  • internal/auditlog/store_postgresql.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • internal/auditlog/auditlog.go
🧬 Code graph analysis (3)
internal/auditlog/store_sqlite.go (3)
internal/auditlog/cleanup.go (1)
  • RunCleanupLoop (13-28)
internal/auditlog/auditlog.go (1)
  • LogEntry (30-58)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/auditlog/store_postgresql.go (3)
internal/auditlog/cleanup.go (1)
  • RunCleanupLoop (13-28)
internal/auditlog/auditlog.go (1)
  • LogEntry (30-58)
internal/core/types.go (1)
  • Model (56-61)
internal/auditlog/auditlog.go (2)
internal/core/types.go (1)
  • Model (56-61)
internal/server/http.go (1)
  • Config (25-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (10)
internal/auditlog/cleanup.go (1)

8-27: Cleanup loop lifecycle looks solid.

Immediate run, ticker shutdown, and stop-channel exit are handled cleanly.

internal/auditlog/store_sqlite.go (3)

21-83: SQLite store initialization looks solid.

Schema/index creation and cleanup-loop wiring are clear.


138-141: Flush no-op is appropriate for synchronous SQLite writes.


143-153: Close guard with sync.Once looks good.

internal/auditlog/store_postgresql.go (2)

89-173: Batch write paths and error aggregation look consistent.


175-211: Lifecycle cleanup/close handling looks solid.

internal/auditlog/auditlog.go (4)

13-58: LogStore interface and LogEntry schema are clear.


91-104: marshalLogData fallback behavior is sensible.


106-144: Header redaction set and copy logic look good.


146-182: Config defaults are reasonable and self-explanatory.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +8 to +14
"WebFetch(domain:docs.x.ai)",
"Bash(go build:*)",
"Bash(golangci-lint run:*)",
"Bash(make test-e2e:*)",
"WebSearch",
"Bash(go doc:*)",
"Bash(grep:*)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and explore the .claude directory
git ls-files | grep -E "\.claude|claude" | head -20

Repository: ENTERPILOT/GOModel

Length of output: 89


🏁 Script executed:

# Check .gitignore for .claude directory rules
cat .gitignore 2>/dev/null | grep -i claude || echo "No claude entries in .gitignore or file not found"

Repository: ENTERPILOT/GOModel

Length of output: 111


🏁 Script executed:

# Look at the actual file content
if [ -f .claude/settings.local.json ]; then
  cat .claude/settings.local.json
else
  echo "File not found at .claude/settings.local.json"
fi

Repository: ENTERPILOT/GOModel

Length of output: 438


🏁 Script executed:

# Check git history for this file to understand intent
git log --oneline .claude/settings.local.json 2>/dev/null | head -10 || echo "No git history found"

Repository: ENTERPILOT/GOModel

Length of output: 108


🏁 Script executed:

# Search for any documentation or comments about .claude settings
find . -type f \( -name "*.md" -o -name "*.txt" \) -not -path "./.git/*" | xargs grep -l "\.claude\|settings\.local" 2>/dev/null | head -5

Repository: ENTERPILOT/GOModel

Length of output: 44


🏁 Script executed:

# Search for any references to Claude settings or documentation
find . -type f \( -name "*.md" -o -name "*.yml" -o -name "*.yaml" -o -name "README*" \) -not -path "./.git/*" | xargs grep -l "claude\|Claude" 2>/dev/null | head -10

Repository: ENTERPILOT/GOModel

Length of output: 130


🏁 Script executed:

# Check if there are other .claude config files that provide context
git ls-files | grep "\.claude"

Repository: ENTERPILOT/GOModel

Length of output: 89


🏁 Script executed:

# Search for any comments or context about what this settings file controls
git log --all -p .claude/settings.local.json 2>/dev/null | head -100 || echo "No detailed history"

Repository: ENTERPILOT/GOModel

Length of output: 2631


🏁 Script executed:

# Read CLAUDE.md to understand the purpose and intent
cat CLAUDE.md

Repository: ENTERPILOT/GOModel

Length of output: 7518


🏁 Script executed:

# Also check README.md for any mention of Claude settings
head -100 README.md | grep -A 5 -B 5 -i "claude\|settings" || tail -100 README.md | grep -A 5 -B 5 -i "claude\|settings"

Repository: ENTERPILOT/GOModel

Length of output: 333


🏁 Script executed:

# Check if there are any other Claude-related files or documentation
grep -r "permissions\|allow\|ask\|deny" CLAUDE.md EXAMPLES.md 2>/dev/null | head -20

Repository: ENTERPILOT/GOModel

Length of output: 44


🏁 Script executed:

# Verify if there are any comments in the actual settings file about purpose
cat .claude/settings.local.json

Repository: ENTERPILOT/GOModel

Length of output: 438


Unnecessary unrestricted permissions should be narrowed or moved to "ask".

The file is intentionally tracked as part of Claude Code integration (documented in CLAUDE.md), so it's appropriate to share. However, WebSearch (unrestricted, no domain limit) and Bash(grep:*) are broader than necessary for the documented workflow. Consider moving these to "ask" to maintain tighter safeguards while preserving the core development tools (test, lint, build, fetch):

🔧 Suggested tightening
   "allow": [
     "Bash(make test-all)",
     "Bash(make lint)",
     "Bash(go test:*)",
     "Bash(make test:*)",
     "WebFetch(domain:docs.x.ai)",
     "Bash(go build:*)",
     "Bash(golangci-lint run:*)",
-    "Bash(make test-e2e:*)",
+    "Bash(make test-e2e:*)"
   ],
   "deny": [],
-  "ask": []
+  "ask": [
+    "WebSearch",
+    "Bash(go doc:*)",
+    "Bash(grep:*)"
+  ]
🤖 Prompt for AI Agents
In @.claude/settings.local.json around lines 8 - 14, The permissions list
currently grants overly broad abilities via the "WebSearch" and "Bash(grep:*)"
entries; update the settings by removing or relocating "WebSearch" and
"Bash(grep:*)" from the main allowed list and placing them under the "ask" (or
equivalent permission-prompt) category, or narrow them (e.g., replace
"WebSearch" with a domain-limited WebFetch entry and restrict "Bash(grep:*)" to
specific patterns) so that only the documented tools like
"WebFetch(domain:docs.x.ai)", "Bash(go build:*)", "Bash(golangci-lint run:*)",
"Bash(make test-e2e:*)", and "Bash(go doc:*)" remain unrestricted.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
config/config.go (1)

214-242: Block startup when no provider API keys are configured.

The config loader currently allows an empty provider set, which should be rejected before returning the config.

🛠️ Proposed guard
-	return &cfg, nil
+	if len(cfg.Providers) == 0 {
+		return nil, fmt.Errorf("at least one provider API key is required to start the server")
+	}
+	return &cfg, nil

As per coding guidelines, at least one provider API key is required to start the server.

internal/providers/anthropic/anthropic.go (2)

326-370: Avoid map-based SSE payloads; use typed structs.

These chunks are API response payloads and should be strongly typed for compile‑time safety and schema clarity. As per coding guidelines, request/response payloads must use strongly-typed structs instead of map[string]interface{}.

♻️ Proposed refactor (requires chunk structs)
-            chunk := map[string]interface{}{
-                "id":       sc.msgID,
-                "object":   "chat.completion.chunk",
-                "created":  time.Now().Unix(),
-                "model":    sc.model,
-                "provider": "anthropic",
-                "choices": []map[string]interface{}{
-                    {
-                        "index": 0,
-                        "delta": map[string]interface{}{
-                            "content": event.Delta.Text,
-                        },
-                        "finish_reason": nil,
-                    },
-                },
-            }
+            chunk := chatCompletionChunk{
+                ID:       sc.msgID,
+                Object:   "chat.completion.chunk",
+                Created:  time.Now().Unix(),
+                Model:    sc.model,
+                Provider: "anthropic",
+                Choices: []chatCompletionChoice{
+                    {
+                        Index:        0,
+                        Delta:        chatCompletionDelta{Content: event.Delta.Text},
+                        FinishReason: nil,
+                    },
+                },
+            }
-            chunk := map[string]interface{}{
-                "id":       sc.msgID,
-                "object":   "chat.completion.chunk",
-                "created":  time.Now().Unix(),
-                "model":    sc.model,
-                "provider": "anthropic",
-                "choices": []map[string]interface{}{
-                    {
-                        "index":         0,
-                        "delta":         map[string]interface{}{},
-                        "finish_reason": event.Delta.StopReason,
-                    },
-                },
-            }
+            finish := event.Delta.StopReason
+            chunk := chatCompletionChunk{
+                ID:       sc.msgID,
+                Object:   "chat.completion.chunk",
+                Created:  time.Now().Unix(),
+                Model:    sc.model,
+                Provider: "anthropic",
+                Choices: []chatCompletionChoice{
+                    {
+                        Index:        0,
+                        Delta:        chatCompletionDelta{},
+                        FinishReason: &finish,
+                    },
+                },
+            }

Consider adding (near the other DTOs):

type chatCompletionChunk struct {
	ID       string                 `json:"id"`
	Object   string                 `json:"object"`
	Created  int64                  `json:"created"`
	Model    string                 `json:"model"`
	Provider string                 `json:"provider"`
	Choices  []chatCompletionChoice `json:"choices"`
}
type chatCompletionChoice struct {
	Index        int                  `json:"index"`
	Delta        chatCompletionDelta  `json:"delta"`
	FinishReason *string              `json:"finish_reason"`
}
type chatCompletionDelta struct {
	Content string `json:"content,omitempty"`
}

As per coding guidelines.


612-623: Use typed structs for Responses SSE events too.

Both response.done and response.created payloads are API responses; mapping them with map[string]interface{} undermines type safety and violates the typed payload guideline. As per coding guidelines, prefer explicit structs.

Also applies to: 690-698

🤖 Fix all issues with AI agents
In `@internal/auditlog/cleanup.go`:
- Around line 1-28: The RunCleanupLoop currently calls cleanupFn directly and a
panic inside cleanupFn would kill the cleanup goroutine; wrap each invocation of
cleanupFn (both the initial call and the call inside the ticker case) in a small
panic-recovery wrapper so panics are recovered, logged, and do not stop the
loop—use the RunCleanupLoop stop channel and existing ticker to continue after
recovery; reference the RunCleanupLoop function, the cleanupFn parameter, ticker
and stop channel when locating where to add the recover logic.

In `@internal/auditlog/constants.go`:
- Around line 4-24: The constants MaxBodyCapture and MaxContentCapture have
inconsistent typing (MaxBodyCapture is int64 while MaxContentCapture is
untyped); make them consistent by declaring MaxContentCapture with the same type
as MaxBodyCapture (int64) or remove the explicit type on MaxBodyCapture so both
are untyped—update the declarations for MaxBodyCapture and MaxContentCapture in
internal/auditlog/constants.go (referencing the symbols MaxBodyCapture and
MaxContentCapture) so they share the same type.

In `@internal/auditlog/store_mongodb.go`:
- Around line 133-149: Replace the direct type assertion of err to
mongo.BulkWriteException with errors.As to unwrap wrapped errors: declare var
bulkErr mongo.BulkWriteException and use if errors.As(err, &bulkErr) {
failedCount := len(bulkErr.WriteErrors); slog.Warn(..., "failed", failedCount,
"succeeded", len(entries)-failedCount); auditLogPartialWriteFailures.Inc();
return &PartialWriteError{TotalEntries: len(entries), FailedCount: failedCount,
Cause: bulkErr} } and ensure the errors package is imported; this targets the
error handling around the InsertMany call and the PartialWriteError return.

In `@internal/providers/init.go`:
- Around line 139-158: The slog.Info call in the cacheType "redis" branch is
logging cfg.Cache.Redis.URL which may contain credentials; change the logging to
parse and redact the URL via net/url's Parse and Redacted() (e.g., parse
cfg.Cache.Redis.URL to redisURL and use redisURL.Redacted()), fall back to a
safe placeholder if parsing fails, and update the slog.Info invocation (the
switch on cacheType, the cfg.Cache.Redis.URL usage, and the slog.Info("using
redis cache", ...) near the cache.NewRedisCache call) to log only the redacted
URL and key.

In `@internal/providers/registry.go`:
- Around line 344-356: GetProviderType returns empty for many production
providers because those providers are registered via RegisterProvider but
r.providerTypes is only populated by RegisterProviderWithType; update the
registration path so providerTypes is always populated: modify RegisterProvider
(the function that production providers call in init()) to also set
r.providerTypes[provider.Name()] (or call RegisterProviderWithType internally)
so the providerTypes map contains an entry for every registered provider,
ensuring GetProviderType (on ModelRegistry) can return the correct type.

In `@internal/providers/router.go`:
- Around line 117-121: GetProviderType currently delegates straight to
r.registry.GetProviderType without calling checkReady(), causing inconsistency
with other Router methods; update Router.GetProviderType to call r.checkReady()
(or return an error if checkReady fails) before accessing r.registry so the
registry is guaranteed initialized and callers can distinguish "not initialized"
from "model not found"; reference the Router type, GetProviderType method,
r.checkReady(), and r.registry.GetProviderType when making the change.

In `@internal/server/http.go`:
- Around line 26-31: The struct field LogOnlyModelInteractions is documented as
"default: true" but its zero value is false and code paths where cfg == nil lead
to unfiltered logging; fix by setting LogOnlyModelInteractions to true during
server construction (e.g., in New or NewServer where the config is initialized)
so the default behavior matches the comment, or alternatively update the comment
on LogOnlyModelInteractions to remove "default: true" to accurately reflect
current behavior—modify the constructor function (New) to initialize
cfg.LogOnlyModelInteractions = true when unspecified, and/or update the field
comment to reflect the actual default.
♻️ Duplicate comments (23)
.env.template (1)

43-45: Remove database name from the MONGODB_URL example to avoid conflicting sources of truth.

The template shows a database name in the URL while also defining MONGODB_DATABASE, which is confusing and can lead to mismatched configuration. Suggest keeping the URL host-only and letting MONGODB_DATABASE define the DB.

💡 Proposed change
-# MONGODB_URL=mongodb://localhost:27017/gomodel
+# MONGODB_URL=mongodb://localhost:27017
# MONGODB_DATABASE=gomodel
.claude/settings.local.json (1)

8-14: Narrow overly broad Claude permissions (WebSearch, Bash(grep:*)).

These entries grant unrestricted capabilities; move them to "ask" or narrow their scope to preserve safer defaults.

🔧 Suggested tightening
  "allow": [
    "Bash(make test-all)",
    "Bash(make lint)",
    "Bash(go test:*)",
    "Bash(make test:*)",
    "WebFetch(domain:docs.x.ai)",
    "Bash(go build:*)",
    "Bash(golangci-lint run:*)",
    "Bash(make test-e2e:*)",
-   "WebSearch",
    "Bash(go doc:*)",
-   "Bash(grep:*)"
+   "Bash(go doc:*)"
  ],
  "deny": [],
- "ask": []
+ "ask": [
+   "WebSearch",
+   "Bash(grep:*)"
+]
config/config.go (1)

339-355: ENV overrides for buffer/flush/retention are missing when a config file is used.

LOGGING_BUFFER_SIZE, LOGGING_FLUSH_INTERVAL, and LOGGING_RETENTION_DAYS are ignored in the config-file path, leading to inconsistent precedence vs the no-config path.

💡 Proposed fix
 	if logHeaders := os.Getenv("LOGGING_LOG_HEADERS"); logHeaders != "" {
 		cfg.Logging.LogHeaders = strings.EqualFold(logHeaders, "true") || logHeaders == "1"
 	}
+	if bufferSize := os.Getenv("LOGGING_BUFFER_SIZE"); bufferSize != "" {
+		if size, err := strconv.Atoi(bufferSize); err == nil {
+			cfg.Logging.BufferSize = size
+		}
+	}
+	if flushInterval := os.Getenv("LOGGING_FLUSH_INTERVAL"); flushInterval != "" {
+		if interval, err := strconv.Atoi(flushInterval); err == nil {
+			cfg.Logging.FlushInterval = interval
+		}
+	}
+	if retentionDays := os.Getenv("LOGGING_RETENTION_DAYS"); retentionDays != "" {
+		if days, err := strconv.Atoi(retentionDays); err == nil {
+			cfg.Logging.RetentionDays = days
+		}
+	}
docker-compose.yaml (3)

19-21: Hardcoded database credentials detected.

The POSTGRES_URL contains hardcoded credentials which is flagged by static analysis (CKV_SECRET_4). This has been noted in previous reviews. Consider using environment variable substitution from .env file.


26-27: Header/body logging enabled by default poses privacy risk.

Enabling LOGGING_LOG_BODIES=true and LOGGING_LOG_HEADERS=true by default can capture credentials, tokens, and PII. This was flagged in a previous review.


73-85: MongoDB service lacks authentication.

The MongoDB service is exposed without authentication credentials. This was flagged in a previous review.

internal/server/http.go (2)

67-101: Request log format diverges between modes (already flagged).

The LogOnlyModelInteractions branch uses structured slog output while the else branch uses default RequestLogger formatting. This was raised in a prior review.


111-114: Redundant audit logger Enabled check (already flagged).

auditlog.Middleware performs the Enabled check internally; the extra guard is unnecessary (noted in prior review).

internal/auditlog/auditlog_test.go (1)

252-279: Sleep-based synchronization in tests is flaky (already flagged).

time.Sleep waiting for async flush can cause intermittent failures; prior review suggested a WaitForEntries helper.

Also applies to: 433-494

internal/storage/postgresql.go (2)

31-36: The MaxConns overflow check is overly defensive.

The check maxConns > math.MaxInt32 guards against a theoretical 2.1+ billion connections, which is unrealistic for any practical configuration. This adds unnecessary complexity.

♻️ Consider simplifying by removing the check or documenting a realistic cap
 	// Set connection pool size
 	if cfg.MaxConns > 0 {
-		maxConns := cfg.MaxConns
-		if maxConns > math.MaxInt32 {
-			maxConns = math.MaxInt32
-		}
-		poolCfg.MaxConns = int32(maxConns)
+		// MaxConns is validated at config load time; safe to convert directly
+		poolCfg.MaxConns = int32(cfg.MaxConns)
 	} else {
 		poolCfg.MaxConns = 10 // default
 	}

If you want to keep validation, consider a realistic upper bound (e.g., 1000) and validate at config load time instead.


79-82: Consider documenting intended usage of Pool() method.

The Pool() method provides direct typed access to *pgxpool.Pool but isn't part of the Storage interface. Consumers would need to type-assert to *postgresStorage to use it. Consider documenting whether consumers should prefer PostgreSQLPool() (via interface) or this method.

internal/auditlog/store_sqlite.go (2)

98-123: Nil data converts to empty string instead of SQL NULL.

When marshalLogData returns nil (for missing data), string(dataJSON) produces an empty string "" instead of preserving NULL semantics. This conflates "no data" with "empty JSON".

🐛 Proposed fix to preserve NULL for missing data
 	for i, e := range entries {
 		placeholders[i] = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"

 		dataJSON := marshalLogData(e.Data, e.ID)

 		// Convert bool to int for SQLite
 		streamInt := 0
 		if e.Stream {
 			streamInt = 1
 		}

+		// Preserve NULL for missing data instead of empty string
+		var dataValue interface{}
+		if dataJSON != nil {
+			dataValue = string(dataJSON)
+		}
+
 		values = append(values,
 			e.ID,
 			e.Timestamp.UTC().Format(time.RFC3339Nano),
 			e.DurationNs,
 			e.Model,
 			e.Provider,
 			e.StatusCode,
 			e.RequestID,
 			e.ClientIP,
 			e.Method,
 			e.Path,
 			streamInt,
 			e.PromptTokens,
 			e.CompletionTokens,
 			e.TotalTokens,
 			e.ErrorType,
-			string(dataJSON),
+			dataValue,
 		)
 	}

155-172: Consider adding a timeout context for the cleanup query.

The cleanup() method executes a DELETE query without a timeout. For large tables with many old entries, this could block indefinitely. The PostgreSQL implementation uses a 5-minute timeout for consistency.

♻️ Proposed fix to add timeout context
 func (s *SQLiteStore) cleanup() {
 	if s.retentionDays <= 0 {
 		return
 	}

+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+	defer cancel()
+
 	cutoff := time.Now().AddDate(0, 0, -s.retentionDays).UTC().Format(time.RFC3339)

-	result, err := s.db.Exec("DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
+	result, err := s.db.ExecContext(ctx, "DELETE FROM audit_logs WHERE timestamp < ?", cutoff)
 	if err != nil {
 		slog.Error("failed to cleanup old audit logs", "error", err)
 		return
 	}

 	if rowsAffected, err := result.RowsAffected(); err == nil && rowsAffected > 0 {
 		slog.Info("cleaned up old audit logs", "deleted", rowsAffected)
 	}
 }

Note: You'll need to add "context" and "time" to the imports if not already present.

internal/auditlog/factory.go (1)

145-166: Logger config defaults may duplicate those in NewLogger.

The defaults applied here (BufferSize: 1000, FlushInterval: 5s) may also be applied in NewLogger. This duplication is defensive but creates two sources of truth. Consider documenting that these are fallback defaults or removing them if NewLogger already handles zero values.

♻️ Consider adding a clarifying comment
 // buildLoggerConfig creates an auditlog.Config from config.LogConfig.
 func buildLoggerConfig(logCfg config.LogConfig) Config {
 	cfg := Config{
 		Enabled:               logCfg.Enabled,
 		LogBodies:             logCfg.LogBodies,
 		LogHeaders:            logCfg.LogHeaders,
 		BufferSize:            logCfg.BufferSize,
 		FlushInterval:         time.Duration(logCfg.FlushInterval) * time.Second,
 		RetentionDays:         logCfg.RetentionDays,
 		OnlyModelInteractions: logCfg.OnlyModelInteractions,
 	}

-	// Apply defaults
+	// Apply defaults as a safety net (NewLogger also applies defaults)
 	if cfg.BufferSize <= 0 {
 		cfg.BufferSize = 1000
 	}
 	if cfg.FlushInterval <= 0 {
 		cfg.FlushInterval = 5 * time.Second
 	}

 	return cfg
 }
internal/storage/mongodb.go (1)

70-75: Add timeout to Disconnect to prevent shutdown hangs.

Using context.Background() could block indefinitely if MongoDB is unresponsive during graceful shutdown. Consider adding a timeout context.

♻️ Proposed fix
 func (s *mongoStorage) Close() error {
 	if s.client != nil {
-		return s.client.Disconnect(context.Background())
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		return s.client.Disconnect(ctx)
 	}
 	return nil
 }

Note: You'll need to import "time".

internal/auditlog/logger.go (2)

61-74: Consider adding a metric counter for dropped entries.

When the buffer is full, entries are dropped with only a log warning. For compliance and observability in production, consider adding a Prometheus counter to track dropped entries.

♻️ Optional enhancement
var auditLogDroppedEntries = promauto.NewCounter(
	prometheus.CounterOpts{
		Name: "gomodel_audit_log_dropped_entries_total",
		Help: "Total number of audit log entries dropped due to buffer full",
	},
)

// In Write method, after logging warning:
auditLogDroppedEntries.Inc()

82-93: Race condition: closed flag must be set in Close() before signaling shutdown.

There's a race window between Write() and Close():

  1. Write() checks l.closed.Load() (returns false)
  2. Close() calls close(l.done) (line 86)
  3. flushLoop receives on l.done, sets l.closed.Store(true) (line 123), then closes l.buffer (line 124)
  4. Write() attempts to send on the now-closed l.bufferpanic

Move the closed flag set to Close() before signaling shutdown:

🐛 Proposed fix
 func (l *Logger) Close() error {
+	// Mark as closed first to prevent new writes
+	l.closed.Store(true)
+
 	// Signal the flush loop to stop
 	close(l.done)

 	// Wait for the flush loop to finish
 	l.wg.Wait()

 	// Close the store
 	return l.store.Close()
 }

And remove the redundant set in flushLoop:

 		case <-l.done:
-			// Shutdown: mark as closed before closing buffer to prevent Write() panics
-			l.closed.Store(true)
+			// Shutdown: closed flag already set by Close()
 			close(l.buffer)
internal/auditlog/store_postgresql.go (1)

25-87: Consider adding a timeout for DDL operations during initialization.

The context.Background() on line 30 could block indefinitely if the database is slow or misconfigured during startup. Consider a bounded context for table and index creation.

♻️ Proposed improvement
 func NewPostgreSQLStore(pool *pgxpool.Pool, retentionDays int) (*PostgreSQLStore, error) {
 	if pool == nil {
 		return nil, fmt.Errorf("connection pool is required")
 	}

-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
internal/server/handlers.go (1)

29-70: Well-structured streaming response handler.

The handleStreamingResponse helper properly:

  1. Executes streamFn first and marks as streaming only after success (addressing the previous race condition)
  2. Sets appropriate SSE headers
  3. Wraps the stream for audit logging
  4. Handles cleanup with deferred close

One minor observability gap remains: the io.Copy error on line 68 is silently discarded. While client disconnects are common for SSE, logging at debug level would help diagnose connection issues.

♻️ Optional: Log io.Copy errors for observability
 	c.Response().WriteHeader(http.StatusOK)
-	_, _ = io.Copy(c.Response().Writer, wrappedStream)
+	if _, err := io.Copy(c.Response().Writer, wrappedStream); err != nil {
+		// Debug level - client disconnects are expected for SSE
+		c.Logger().Debug("stream copy ended", "error", err)
+	}
 	return nil
tests/e2e/auditlog_test.go (2)

107-155: Add a post-loop readiness assertion.

The loop can exit without confirming readiness; tests may proceed while the server is still down.

✅ Proposed fix
 	// Wait for server to be ready
 	client := &http.Client{Timeout: 2 * time.Second}
+	serverReady := false
 	for i := 0; i < 30; i++ {
 		resp, err := client.Get(serverURL + "/health")
 		if err == nil {
 			_ = resp.Body.Close()
 			if resp.StatusCode == http.StatusOK {
+				serverReady = true
 				break
 			}
 		}
 		time.Sleep(100 * time.Millisecond)
 	}
+	require.True(t, serverReady, "server did not become ready at %s within expected time", serverURL)

204-245: Use typed request body assertions instead of map[string]interface{}.

Prefer decoding into core.ChatRequest to keep payloads strongly typed. As per coding guidelines, prefer strongly‑typed payload structs.

✅ Proposed fix
-		// Verify request body contains our message (now stored as interface{})
-		reqBody, ok := entry.Data.RequestBody.(map[string]interface{})
-		require.True(t, ok, "RequestBody should be a map[string]interface{}, got %T", entry.Data.RequestBody)
-		assert.Equal(t, "gpt-4", reqBody["model"])
+		// Verify request body contains our message using a typed struct
+		raw, err := json.Marshal(entry.Data.RequestBody)
+		require.NoError(t, err)
+		var reqBody core.ChatRequest
+		require.NoError(t, json.Unmarshal(raw, &reqBody))
+		assert.Equal(t, "gpt-4", reqBody.Model)
internal/storage/storage.go (1)

56-78: Consider typed accessors to avoid interface{} casting.

Returning interface{} forces downstream type assertions; exposing typed accessors on concrete storage types (or documenting the casting pattern) would reduce friction. Based on learnings, this is optional for internal infra but improves ergonomics.

internal/auditlog/auditlog.go (1)

60-89: Avoid interface{} for logged request/response bodies.

Use a typed wrapper (e.g., LogBody with json.RawMessage or a typed union) so payloads remain strongly typed. As per coding guidelines, request/response payloads must be strongly typed.

✅ Minimal compliant direction
 type LogData struct {
@@
-	RequestBody  interface{} `json:"request_body,omitempty" bson:"request_body,omitempty"`
-	ResponseBody interface{} `json:"response_body,omitempty" bson:"response_body,omitempty"`
+	RequestBody  *LogBody `json:"request_body,omitempty" bson:"request_body,omitempty"`
+	ResponseBody *LogBody `json:"response_body,omitempty" bson:"response_body,omitempty"`
@@
 }
+
+// LogBody wraps raw JSON while keeping a typed field.
+type LogBody struct {
+	JSON json.RawMessage `json:"json,omitempty" bson:"json,omitempty"`
+}
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82c5db7 and f4ebd2d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (40)
  • .claude/settings.local.json
  • .env.template
  • cmd/gomodel/main.go
  • config/config.go
  • config/config_test.go
  • docker-compose.yaml
  • go.mod
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/cleanup.go
  • internal/auditlog/constants.go
  • internal/auditlog/factory.go
  • internal/auditlog/logger.go
  • internal/auditlog/middleware.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/stream_wrapper.go
  • internal/core/interfaces.go
  • internal/core/responses.go
  • internal/core/types.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/gemini/gemini.go
  • internal/providers/groq/groq.go
  • internal/providers/groq/groq_test.go
  • internal/providers/init.go
  • internal/providers/openai/openai.go
  • internal/providers/registry.go
  • internal/providers/responses_converter.go
  • internal/providers/router.go
  • internal/providers/xai/xai.go
  • internal/server/handlers.go
  • internal/server/handlers_test.go
  • internal/server/http.go
  • internal/storage/mongodb.go
  • internal/storage/postgresql.go
  • internal/storage/sqlite.go
  • internal/storage/storage.go
  • tests/e2e/auditlog_test.go
  • tests/e2e/main_test.go
🧰 Additional context used
📓 Path-based instructions (11)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time
Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles
Use the module name gomodel for all imports and package references

Files:

  • tests/e2e/main_test.go
  • internal/providers/router.go
  • internal/core/interfaces.go
  • internal/auditlog/auditlog_test.go
  • internal/providers/registry.go
  • internal/auditlog/cleanup.go
  • config/config_test.go
  • internal/providers/openai/openai.go
  • internal/storage/postgresql.go
  • internal/providers/gemini/gemini.go
  • internal/server/http.go
  • internal/auditlog/store_postgresql.go
  • internal/server/handlers_test.go
  • internal/core/responses.go
  • internal/providers/groq/groq_test.go
  • internal/storage/sqlite.go
  • internal/auditlog/logger.go
  • internal/auditlog/factory.go
  • internal/auditlog/store_sqlite.go
  • tests/e2e/auditlog_test.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/middleware.go
  • internal/server/handlers.go
  • cmd/gomodel/main.go
  • internal/providers/xai/xai.go
  • internal/providers/anthropic/anthropic.go
  • config/config.go
  • internal/providers/groq/groq.go
  • internal/auditlog/auditlog.go
  • internal/storage/mongodb.go
  • internal/storage/storage.go
  • internal/providers/init.go
  • internal/providers/responses_converter.go
  • internal/core/types.go
  • internal/auditlog/constants.go
  • internal/auditlog/stream_wrapper.go
**/*_test.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use unit tests alongside implementation files with *_test.go naming convention for internal and config packages

Files:

  • tests/e2e/main_test.go
  • internal/auditlog/auditlog_test.go
  • config/config_test.go
  • internal/server/handlers_test.go
  • internal/providers/groq/groq_test.go
  • tests/e2e/auditlog_test.go
tests/e2e/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests must use the -tags=e2e build tag and use the in-process mock LLM server without Docker or external dependencies

Files:

  • tests/e2e/main_test.go
  • tests/e2e/auditlog_test.go
internal/providers/{router,registry}.go

📄 CodeRabbit inference engine (CLAUDE.md)

The ModelRegistry must be initialized before routing requests; check ModelCount() > 0 or use router after InitializeAsync() completes to avoid ErrRegistryNotInitialized errors

Files:

  • internal/providers/router.go
  • internal/providers/registry.go
internal/providers/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Streaming responses must return io.ReadCloser and callers are responsible for closing the stream

Files:

  • internal/providers/router.go
  • internal/providers/registry.go
  • internal/providers/openai/openai.go
  • internal/providers/gemini/gemini.go
  • internal/providers/groq/groq_test.go
  • internal/providers/xai/xai.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/groq/groq.go
  • internal/providers/init.go
  • internal/providers/responses_converter.go
internal/providers/router.go

📄 CodeRabbit inference engine (CLAUDE.md)

When multiple providers support the same model, the first registered provider wins the routing decision

Files:

  • internal/providers/router.go
internal/providers/registry.go

📄 CodeRabbit inference engine (CLAUDE.md)

internal/providers/registry.go: Use RWMutex for thread-safe concurrent access to the ModelRegistry
Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Files:

  • internal/providers/registry.go
internal/server/**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

Use Echo (v4) framework for HTTP server setup and middleware, not Chi or other frameworks

Files:

  • internal/server/http.go
  • internal/server/handlers_test.go
  • internal/server/handlers.go
go.mod

📄 CodeRabbit inference engine (CLAUDE.md)

Go version must be 1.24.0 or later

Files:

  • go.mod
cmd/gomodel/main.go

📄 CodeRabbit inference engine (CLAUDE.md)

Provider packages must be imported in cmd/gomodel/main.go with blank imports (e.g., _ "gomodel/internal/providers/openai") to trigger their init() registration

Files:

  • cmd/gomodel/main.go
config/config.go

📄 CodeRabbit inference engine (CLAUDE.md)

config/config.go: Use Viper for loading configuration from environment variables and .env files in the config package
At least one provider API key is required in the environment configuration to start the server

Files:

  • config/config.go
🧠 Learnings (21)
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to tests/e2e/**/*.go : E2E tests must use the `-tags=e2e` build tag and use the in-process mock LLM server without Docker or external dependencies

Applied to files:

  • tests/e2e/main_test.go
  • tests/e2e/auditlog_test.go
  • go.mod
📚 Learning: 2026-01-15T23:07:37.652Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/factory.go:112-143
Timestamp: 2026-01-15T23:07:37.652Z
Learning: Guideline: Do not use interface{} or map[string]interface{} for API request/response payload types. Prefer strongly-typed structs for API payload definitions to improve type safety, serialization, and documentation. Allow interface{} only in internal infrastructure code paths where pragmatic flexibility is necessary (e.g., to avoid import cycles or to handle highly dynamic internal contracts). In internal/auditlog/factory.go and similar non-API implementation files, applying this restriction is optional and should be evaluated on a case-by-case basis based on whether the type remains internal and does not define API boundary shapes.

Applied to files:

  • tests/e2e/main_test.go
  • internal/providers/router.go
  • internal/core/interfaces.go
  • internal/auditlog/auditlog_test.go
  • internal/providers/registry.go
  • internal/auditlog/cleanup.go
  • config/config_test.go
  • internal/providers/openai/openai.go
  • internal/storage/postgresql.go
  • internal/providers/gemini/gemini.go
  • internal/server/http.go
  • internal/auditlog/store_postgresql.go
  • internal/server/handlers_test.go
  • internal/core/responses.go
  • internal/providers/groq/groq_test.go
  • internal/storage/sqlite.go
  • internal/auditlog/logger.go
  • internal/auditlog/factory.go
  • internal/auditlog/store_sqlite.go
  • tests/e2e/auditlog_test.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/middleware.go
  • internal/server/handlers.go
  • cmd/gomodel/main.go
  • internal/providers/xai/xai.go
  • internal/providers/anthropic/anthropic.go
  • config/config.go
  • internal/providers/groq/groq.go
  • internal/auditlog/auditlog.go
  • internal/storage/mongodb.go
  • internal/storage/storage.go
  • internal/providers/init.go
  • internal/providers/responses_converter.go
  • internal/core/types.go
  • internal/auditlog/constants.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/router.go : When multiple providers support the same model, the first registered provider wins the routing decision

Applied to files:

  • internal/providers/router.go
  • internal/core/interfaces.go
  • internal/providers/registry.go
  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/{router,registry}.go : The ModelRegistry must be initialized before routing requests; check `ModelCount() > 0` or use router after `InitializeAsync()` completes to avoid `ErrRegistryNotInitialized` errors

Applied to files:

  • internal/providers/router.go
  • internal/providers/registry.go
  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Provider packages must implement the `core.Provider` interface defined in `internal/core/interfaces.go` with methods: ChatCompletion, StreamChatCompletion, ListModels, Responses, and StreamResponses

Applied to files:

  • internal/providers/router.go
  • internal/core/interfaces.go
  • internal/providers/registry.go
  • internal/providers/openai/openai.go
  • internal/providers/gemini/gemini.go
  • internal/server/handlers_test.go
  • internal/core/responses.go
  • internal/providers/groq/groq_test.go
  • tests/e2e/auditlog_test.go
  • internal/server/handlers.go
  • internal/providers/xai/xai.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/groq/groq.go
  • internal/providers/init.go
  • internal/providers/responses_converter.go
  • internal/core/types.go
📚 Learning: 2026-01-15T23:07:26.646Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/auditlog_test.go:272-279
Timestamp: 2026-01-15T23:07:26.646Z
Learning: Go code guideline: Avoid using interface{} and map[string]interface{} for API request/response payload types. This should be restricted to public API surfaces and data payloads. It is acceptable to use interface{} in internal infrastructure packages (e.g., storage abstractions) only to break import cycles, and only when type assertions occur at initialization (not on every request) to minimize runtime type checks and improve readability. Apply this rule broadly to files under internal/ that deal with payload-like types, but not to internal components that are pure infrastructure or have runtime-per-request type assertions.

Applied to files:

  • internal/providers/router.go
  • internal/core/interfaces.go
  • internal/auditlog/auditlog_test.go
  • internal/providers/registry.go
  • internal/auditlog/cleanup.go
  • internal/providers/openai/openai.go
  • internal/storage/postgresql.go
  • internal/providers/gemini/gemini.go
  • internal/server/http.go
  • internal/auditlog/store_postgresql.go
  • internal/server/handlers_test.go
  • internal/core/responses.go
  • internal/providers/groq/groq_test.go
  • internal/storage/sqlite.go
  • internal/auditlog/logger.go
  • internal/auditlog/factory.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/middleware.go
  • internal/server/handlers.go
  • internal/providers/xai/xai.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/groq/groq.go
  • internal/auditlog/auditlog.go
  • internal/storage/mongodb.go
  • internal/storage/storage.go
  • internal/providers/init.go
  • internal/providers/responses_converter.go
  • internal/core/types.go
  • internal/auditlog/constants.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*_test.go : Use unit tests alongside implementation files with `*_test.go` naming convention for internal and config packages

Applied to files:

  • internal/auditlog/auditlog_test.go
  • config/config_test.go
  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Use RWMutex for thread-safe concurrent access to the ModelRegistry

Applied to files:

  • internal/auditlog/auditlog_test.go
  • internal/providers/registry.go
  • cmd/gomodel/main.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : All request/response payloads must use strongly-typed structs instead of interfaces{} or map[string]interface{} to catch errors at compile time

Applied to files:

  • internal/auditlog/auditlog_test.go
  • internal/auditlog/factory.go
  • tests/e2e/auditlog_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/registry.go : Provider model registration loads from cache first synchronously, then refreshes asynchronously in the background every 5 minutes

Applied to files:

  • internal/providers/registry.go
  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to cmd/gomodel/main.go : Provider packages must be imported in `cmd/gomodel/main.go` with blank imports (e.g., `_ "gomodel/internal/providers/openai"`) to trigger their init() registration

Applied to files:

  • internal/providers/registry.go
  • internal/providers/openai/openai.go
  • internal/providers/gemini/gemini.go
  • cmd/gomodel/main.go
  • internal/providers/xai/xai.go
  • internal/providers/init.go
  • internal/providers/responses_converter.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : Each provider package must include an `init()` function that registers itself with the factory using `providers.RegisterFactory("provider-name", NewProvider)`

Applied to files:

  • internal/providers/registry.go
  • cmd/gomodel/main.go
  • internal/providers/init.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : Use `Viper` for loading configuration from environment variables and .env files in the config package

Applied to files:

  • config/config_test.go
  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/*/provider.go : The responses converter in `internal/providers/responses_converter.go` must handle the OpenAI→Responses stream conversion for all providers

Applied to files:

  • internal/providers/openai/openai.go
  • internal/providers/gemini/gemini.go
  • internal/core/responses.go
  • internal/providers/groq/groq_test.go
  • internal/server/handlers.go
  • internal/providers/xai/xai.go
  • internal/providers/anthropic/anthropic.go
  • internal/providers/groq/groq.go
  • internal/providers/responses_converter.go
  • internal/core/types.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to **/*.go : Use goroutines and channels for high-throughput scenarios (10k+ concurrent connections) to maintain concurrency design principles

Applied to files:

  • internal/storage/postgresql.go
  • internal/auditlog/store_postgresql.go
  • internal/storage/sqlite.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/providers/**/*.go : Streaming responses must return `io.ReadCloser` and callers are responsible for closing the stream

Applied to files:

  • internal/providers/gemini/gemini.go
  • internal/core/responses.go
  • internal/providers/groq/groq_test.go
  • internal/auditlog/middleware.go
  • internal/server/handlers.go
  • internal/providers/groq/groq.go
  • internal/providers/responses_converter.go
  • internal/auditlog/stream_wrapper.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/server/**/*.go : Use **Echo (v4)** framework for HTTP server setup and middleware, not Chi or other frameworks

Applied to files:

  • internal/server/http.go
  • go.mod
  • internal/auditlog/middleware.go
📚 Learning: 2026-01-15T23:07:26.646Z
Learnt from: SantiagoDePolonia
Repo: ENTERPILOT/GOModel PR: 33
File: internal/auditlog/auditlog_test.go:272-279
Timestamp: 2026-01-15T23:07:26.646Z
Learning: In Go code, the strict typing guideline (avoiding interface{} and map[string]interface{}) applies specifically to API request/response payloads, not to internal infrastructure interfaces. Using interface{} in internal packages (e.g., storage abstractions) is acceptable when it avoids import cycles, especially when type assertions occur once at initialization rather than on every request.

Applied to files:

  • tests/e2e/auditlog_test.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to go.mod : Go version must be 1.24.0 or later

Applied to files:

  • go.mod
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to internal/cache/**/*.go : Cache structure must use `ModelCache` struct with Version, UpdatedAt, and Models (map[string]CachedModel) fields

Applied to files:

  • cmd/gomodel/main.go
  • config/config.go
📚 Learning: 2025-12-28T22:10:31.364Z
Learnt from: CR
Repo: ENTERPILOT/GOModel PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:10:31.364Z
Learning: Applies to config/config.go : At least one provider API key is required in the environment configuration to start the server

Applied to files:

  • cmd/gomodel/main.go
🧬 Code graph analysis (26)
internal/auditlog/auditlog_test.go (2)
internal/auditlog/auditlog.go (4)
  • RedactHeaders (130-144)
  • LogEntry (30-58)
  • LogData (63-89)
  • Config (147-169)
internal/auditlog/stream_wrapper.go (4)
  • IsModelInteractionPath (521-533)
  • NewStreamLogWrapper (55-85)
  • WrapStreamForLogging (430-435)
  • CreateStreamEntry (439-474)
internal/providers/openai/openai.go (6)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/anthropic/anthropic.go (1)
  • Provider (34-37)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/xai/xai.go (1)
  • Provider (24-27)
internal/core/types.go (1)
  • Model (56-61)
internal/storage/postgresql.go (1)
internal/storage/storage.go (3)
  • PostgreSQLConfig (41-46)
  • Storage (58-78)
  • TypePostgreSQL (15-15)
internal/providers/gemini/gemini.go (3)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/core/types.go (1)
  • Model (56-61)
internal/providers/responses_converter.go (1)
  • NewOpenAIResponsesStreamConverter (31-40)
internal/server/http.go (6)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/core/interfaces.go (1)
  • RoutableProvider (30-39)
internal/auditlog/auditlog.go (1)
  • Config (147-169)
internal/server/handlers.go (1)
  • NewHandler (22-27)
internal/auditlog/stream_wrapper.go (1)
  • IsModelInteractionPath (521-533)
internal/auditlog/middleware.go (1)
  • Middleware (29-159)
internal/auditlog/store_postgresql.go (3)
internal/auditlog/cleanup.go (1)
  • RunCleanupLoop (13-28)
internal/auditlog/auditlog.go (1)
  • LogEntry (30-58)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/server/handlers_test.go (1)
internal/server/handlers.go (1)
  • NewHandler (22-27)
internal/core/responses.go (6)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/anthropic/anthropic.go (1)
  • Provider (34-37)
internal/providers/gemini/gemini.go (1)
  • Provider (31-35)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/openai/openai.go (1)
  • Provider (24-27)
internal/providers/xai/xai.go (1)
  • Provider (24-27)
internal/providers/groq/groq_test.go (1)
internal/providers/responses_converter.go (1)
  • NewOpenAIResponsesStreamConverter (31-40)
internal/storage/sqlite.go (1)
internal/storage/storage.go (3)
  • SQLiteConfig (35-38)
  • Storage (58-78)
  • TypeSQLite (14-14)
internal/auditlog/logger.go (3)
internal/auditlog/auditlog.go (3)
  • LogStore (15-26)
  • Config (147-169)
  • LogEntry (30-58)
internal/server/http.go (1)
  • Config (25-32)
internal/auditlog/constants.go (1)
  • BatchFlushThreshold (19-19)
internal/auditlog/store_sqlite.go (3)
internal/auditlog/cleanup.go (1)
  • RunCleanupLoop (13-28)
internal/auditlog/auditlog.go (1)
  • LogEntry (30-58)
internal/core/errors.go (1)
  • ErrorType (11-11)
tests/e2e/auditlog_test.go (3)
internal/auditlog/auditlog.go (2)
  • LogEntry (30-58)
  • Config (147-169)
internal/auditlog/logger.go (2)
  • Logger (14-22)
  • NewLogger (26-46)
internal/auditlog/factory.go (1)
  • New (47-79)
internal/auditlog/store_mongodb.go (2)
internal/storage/storage.go (1)
  • New (82-93)
internal/auditlog/auditlog.go (1)
  • LogEntry (30-58)
internal/auditlog/middleware.go (4)
internal/auditlog/auditlog.go (3)
  • LogEntry (30-58)
  • LogData (63-89)
  • RedactHeaders (130-144)
internal/auditlog/stream_wrapper.go (2)
  • IsModelInteractionPath (521-533)
  • IsEntryMarkedAsStreaming (511-518)
internal/auditlog/constants.go (3)
  • MaxBodyCapture (7-7)
  • LogEntryKey (31-31)
  • APIKeyHashPrefixLength (23-23)
internal/core/types.go (2)
  • Usage (49-53)
  • Model (56-61)
internal/server/handlers.go (4)
internal/core/interfaces.go (1)
  • RoutableProvider (30-39)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/auditlog/stream_wrapper.go (4)
  • MarkEntryAsStreaming (506-508)
  • GetStreamEntryFromContext (490-502)
  • CreateStreamEntry (439-474)
  • WrapStreamForLogging (430-435)
internal/auditlog/middleware.go (1)
  • EnrichEntry (230-249)
cmd/gomodel/main.go (5)
internal/providers/init.go (1)
  • Init (64-66)
internal/auditlog/factory.go (1)
  • New (47-79)
internal/server/http.go (3)
  • New (35-136)
  • Config (25-32)
  • Server (19-22)
internal/auditlog/auditlog.go (1)
  • Config (147-169)
config/config.go (1)
  • Config (26-33)
internal/providers/anthropic/anthropic.go (5)
internal/core/types.go (1)
  • Model (56-61)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/groq/groq.go (1)
  • Provider (27-30)
internal/providers/openai/openai.go (1)
  • Provider (24-27)
internal/providers/xai/xai.go (1)
  • Provider (24-27)
config/config.go (1)
internal/storage/storage.go (1)
  • Storage (58-78)
internal/providers/groq/groq.go (2)
internal/core/interfaces.go (1)
  • Provider (10-25)
internal/providers/responses_converter.go (1)
  • NewOpenAIResponsesStreamConverter (31-40)
internal/auditlog/auditlog.go (3)
internal/core/types.go (1)
  • Model (56-61)
internal/core/errors.go (1)
  • ErrorType (11-11)
internal/server/http.go (1)
  • Config (25-32)
internal/storage/mongodb.go (1)
internal/storage/storage.go (3)
  • MongoDBConfig (49-54)
  • Storage (58-78)
  • TypeMongoDB (16-16)
internal/storage/storage.go (3)
internal/storage/sqlite.go (1)
  • NewSQLite (19-49)
internal/storage/postgresql.go (1)
  • NewPostgreSQL (19-54)
internal/storage/mongodb.go (1)
  • NewMongoDB (19-52)
internal/providers/init.go (6)
internal/providers/registry.go (2)
  • ModelRegistry (25-33)
  • NewModelRegistry (36-41)
internal/providers/router.go (2)
  • Router (18-20)
  • NewRouter (25-32)
internal/cache/cache.go (1)
  • Cache (28-38)
internal/cache/redis.go (2)
  • DefaultRedisTTL (19-19)
  • NewRedisCache (43-76)
internal/cache/local.go (1)
  • NewLocalCache (21-25)
internal/providers/factory.go (1)
  • Create (41-47)
internal/core/types.go (1)
internal/auditlog/middleware.go (1)
  • Usage (343-347)
internal/auditlog/stream_wrapper.go (4)
internal/auditlog/logger.go (1)
  • LoggerInterface (178-182)
internal/auditlog/auditlog.go (3)
  • LogEntry (30-58)
  • Config (147-169)
  • LogData (63-89)
internal/auditlog/constants.go (4)
  • SSEBufferSize (15-15)
  • MaxContentCapture (11-11)
  • LogEntryKey (31-31)
  • LogEntryStreamingKey (35-35)
internal/auditlog/middleware.go (1)
  • Usage (343-347)
🪛 Checkov (3.2.334)
.env.template

[medium] 40-41: Basic Auth Credentials

(CKV_SECRET_4)

docker-compose.yaml

[medium] 19-20: Basic Auth Credentials

(CKV_SECRET_4)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +1 to +28
package auditlog

import "time"

// NOTE: If more shared store functions are added beyond cleanup,
// consider renaming this file to store_common.go or similar.

// CleanupInterval is how often the cleanup goroutine runs to delete old log entries.
const CleanupInterval = 1 * time.Hour

// RunCleanupLoop runs a cleanup function periodically until the stop channel is closed.
// It runs cleanup immediately on start, then at CleanupInterval intervals.
func RunCleanupLoop(stop <-chan struct{}, cleanupFn func()) {
ticker := time.NewTicker(CleanupInterval)
defer ticker.Stop()

// Run initial cleanup
cleanupFn()

for {
select {
case <-ticker.C:
cleanupFn()
case <-stop:
return
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LGTM! Clean goroutine-based periodic cleanup implementation.

The implementation follows idiomatic Go patterns:

  • Uses channels for graceful shutdown coordination
  • Properly defers ticker.Stop() to prevent resource leaks
  • Runs cleanup immediately on start (useful for catching up after restarts)

Consider adding panic recovery to prevent a misbehaving cleanupFn from crashing the cleanup goroutine:

♻️ Optional: Add panic recovery
 func RunCleanupLoop(stop <-chan struct{}, cleanupFn func()) {
 	ticker := time.NewTicker(CleanupInterval)
 	defer ticker.Stop()

+	safeCleanup := func() {
+		defer func() {
+			if r := recover(); r != nil {
+				slog.Error("cleanup panic recovered", "error", r)
+			}
+		}()
+		cleanupFn()
+	}
+
 	// Run initial cleanup
-	cleanupFn()
+	safeCleanup()

 	for {
 		select {
 		case <-ticker.C:
-			cleanupFn()
+			safeCleanup()
 		case <-stop:
 			return
 		}
 	}
 }
🤖 Prompt for AI Agents
In `@internal/auditlog/cleanup.go` around lines 1 - 28, The RunCleanupLoop
currently calls cleanupFn directly and a panic inside cleanupFn would kill the
cleanup goroutine; wrap each invocation of cleanupFn (both the initial call and
the call inside the ticker case) in a small panic-recovery wrapper so panics are
recovered, logged, and do not stop the loop—use the RunCleanupLoop stop channel
and existing ticker to continue after recovery; reference the RunCleanupLoop
function, the cleanupFn parameter, ticker and stop channel when locating where
to add the recover logic.

Comment on lines +133 to +149
// Check if it's a bulk write error with some successes
if bulkErr, ok := err.(mongo.BulkWriteException); ok {
failedCount := len(bulkErr.WriteErrors)
// Log for visibility
slog.Warn("partial audit log insert failure",
"total", len(entries),
"failed", failedCount,
"succeeded", len(entries)-failedCount,
)
// Increment metric for operators to detect data loss
auditLogPartialWriteFailures.Inc()
// Return distinguishable error so callers know insert was partial
return &PartialWriteError{
TotalEntries: len(entries),
FailedCount: failedCount,
Cause: bulkErr,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, read the actual file to see the current implementation
cat -n internal/auditlog/store_mongodb.go | sed -n '120,160p'

Repository: ENTERPILOT/GOModel

Length of output: 1516


🏁 Script executed:

# Check if the file exists and get its structure
fd -t f "store_mongodb.go" -x wc -l {}

Repository: ENTERPILOT/GOModel

Length of output: 102


🏁 Script executed:

# Search for imports in the file to understand the mongo package usage
head -50 internal/auditlog/store_mongodb.go | grep -E "^import|mongo"

Repository: ENTERPILOT/GOModel

Length of output: 347


🌐 Web query:

mongo-driver-go v2 BulkWriteException error handling errors.As

💡 Result:

Yes — in the mongo-go-driver v2 the bulk error type is mongo.BulkWriteException and you can detect/unpack it with errors.As. Example:

  • Key docs: mongo.BulkWriteException type has WriteErrors, WriteConcernError and helper methods. [1]
  • InsertMany/BulkWrite produce BulkWriteException on errors. [2]

Example code:

import (
    "errors"
    "fmt"
    "go.mongodb.org/mongo-driver/mongo"
)

// err is the error returned by InsertMany or BulkWrite
var bwe mongo.BulkWriteException
if errors.As(err, &bwe) {
    // inspect per-operation errors
    for _, we := range bwe.WriteErrors {
        fmt.Printf("index=%d code=%d msg=%s\n", we.Index, we.Code, we.Message)
    }
    if bwe.WriteConcernError != nil {
        fmt.Printf("write concern error: %s\n", bwe.WriteConcernError.Message)
    }
    // use bwe.HasErrorCode(...) / HasErrorMessage(...) / ErrorCodes() etc.
} else if mongo.IsDuplicateKeyError(err) {
    // other checks...
}

References:

  • mongo.BulkWriteException docs. [1]
  • InsertMany / BulkWrite behavior (BulkWriteException thrown). [2]

Sources:
[1] go.mongodb.org/mongo-driver/v2/mongo (BulkWriteException).
[2] MongoDB Go driver docs — InsertMany / BulkWrite behavior.


🏁 Script executed:

# Search for how BulkWriteException is actually used in the codebase
rg "BulkWriteException" -A 3 -B 3 --type go

Repository: ENTERPILOT/GOModel

Length of output: 1424


🏁 Script executed:

# Check the PartialWriteError struct definition
rg "type PartialWriteError" -A 5 --type go

Repository: ENTERPILOT/GOModel

Length of output: 381


🌐 Web query:

mongo-driver v2 BulkWriteException type definition pointer value struct

💡 Result:

Short answer: BulkWriteException is defined as a concrete struct (not a pointer) with a pointer field for WriteConcernError:

type BulkWriteException struct {
WriteConcernError *WriteConcernError
WriteErrors []BulkWriteError
Labels []string
}

(Definition in the mongo-driver v2 package.) [1][2]

Notes on using it as an error:

  • The type has an Error() method with a value receiver, so both BulkWriteException and *BulkWriteException satisfy the error interface. [1]
  • To extract the concrete error from an error value, use errors.As with a pointer to the concrete type, e.g.:
    var bwe mongo.BulkWriteException
    if errors.As(err, &bwe) { /* use bwe.WriteErrors, bwe.WriteConcernError, etc. */ }

Sources: pkg.go.dev (mongo v2 type docs) and MongoDB Go driver docs. [1][2]

References
[1] pkg.go.dev — go.mongodb.org/mongo-driver/v2/mongo (BulkWriteException type).
[2] MongoDB Go Driver docs — Bulk operations / InsertMany (BulkWriteException discussion).


🌐 Web query:

mongo-driver go v2 InsertMany error return type BulkWriteException wrapped

💡 Result:

Short answer: InsertMany returns (*mongo.InsertManyResult, error). On write failures (e.g. duplicate key(s)) the driver returns a mongo.BulkWriteException (which implements error). Because errors may be wrapped, check it with errors.As (or a type assertion if you know it's not wrapped).

Example:

  • signature: res, err := coll.InsertMany(ctx, docs) — error may be a BulkWriteException. [1]
  • inspect with errors.As:
    var bwe mongo.BulkWriteException
    if errors.As(err, &bwe) {
    // inspect bwe.WriteErrors, bwe.WriteConcernError, bwe.ErrorCodes(), etc.
    } [1][2]

References:

  • mongo-driver v2 API (BulkWriteException / InsertMany docs). [1]
  • MongoDB Go driver InsertMany guide (notes about BulkWriteException and inserted IDs). [2]

🏁 Script executed:

# Search for how mongo-driver patterns are used in the codebase
rg "errors\.As" --type go -B 2 -A 2

Repository: ENTERPILOT/GOModel

Length of output: 1398


🏁 Script executed:

# Check if there are any other error handling examples with mongo in the codebase
rg "InsertMany|BulkWrite" --type go -A 5 -B 1

Repository: ENTERPILOT/GOModel

Length of output: 2195


🌐 Web query:

mongo-driver go v2 InsertMany BulkWriteException wrapped error fmt.Errorf

💡 Result:

Short answer: InsertMany returns a mongo.BulkWriteException for bulk/write errors (duplicate-key etc.). If you wrap that error (fmt.Errorf("%w", err)) you must use errors.As (or errors.Unwrap) to get the underlying mongo.BulkWriteException and inspect its fields (WriteErrors, WriteConcernError, Labels). Example:

  • docs: InsertMany returns BulkWriteException. [1][2]

Example code:

res, err := coll.InsertMany(ctx, docs, options.InsertMany().SetOrdered(false))
if err != nil {
    var bwe mongo.BulkWriteException
    if errors.As(err, &bwe) {
        // handle bulk write errors
        for _, we := range bwe.WriteErrors {
            fmt.Println("index:", we.Index, "code:", we.Code, "msg:", we.Message)
        }
        if bwe.WriteConcernError != nil {
            fmt.Println("write concern error:", bwe.WriteConcernError.Message)
        }
    } else {
        // other errors
        log.Println("insert error:", err)
    }
} else {
    fmt.Println("inserted IDs:", res.InsertedIDs)
}

Notes:

  • Use fmt.Errorf("%w", err) to wrap while preserving the ability to unwrap via errors.As / errors.Is.
  • InsertMany can still return partial InsertedIDs when a BulkWriteException occurs (see docs). [1][2]

Sources:
[1] MongoDB Go driver InsertMany docs.
[2] go.mongodb.org/mongo-driver v2 mongo package (BulkWriteException / types).


Use errors.As with value receiver to extract BulkWriteException without missing wrapped errors.

Type assertions can fail if InsertMany wraps the error (e.g., via fmt.Errorf). Use errors.As with the value type to unwrap and handle partial failures correctly. The PartialWriteError.Cause field expects mongo.BulkWriteException (value type), so extract accordingly:

var bulkErr mongo.BulkWriteException
if errors.As(err, &bulkErr) {
    // Use bulkErr.WriteErrors, etc.
    return &PartialWriteError{
        TotalEntries: len(entries),
        FailedCount:  len(bulkErr.WriteErrors),
        Cause:        bulkErr,
    }
}
🤖 Prompt for AI Agents
In `@internal/auditlog/store_mongodb.go` around lines 133 - 149, Replace the
direct type assertion of err to mongo.BulkWriteException with errors.As to
unwrap wrapped errors: declare var bulkErr mongo.BulkWriteException and use if
errors.As(err, &bulkErr) { failedCount := len(bulkErr.WriteErrors);
slog.Warn(..., "failed", failedCount, "succeeded", len(entries)-failedCount);
auditLogPartialWriteFailures.Inc(); return &PartialWriteError{TotalEntries:
len(entries), FailedCount: failedCount, Cause: bulkErr} } and ensure the errors
package is imported; this targets the error handling around the InsertMany call
and the PartialWriteError return.

Comment on lines +139 to +158
switch cacheType {
case "redis":
ttl := time.Duration(cfg.Cache.Redis.TTL) * time.Second
if ttl == 0 {
ttl = cache.DefaultRedisTTL
}

redisCfg := cache.RedisConfig{
URL: cfg.Cache.Redis.URL,
Key: cfg.Cache.Redis.Key,
TTL: ttl,
}

redisCache, err := cache.NewRedisCache(redisCfg)
if err != nil {
return nil, err
}

slog.Info("using redis cache", "url", cfg.Cache.Redis.URL, "key", cfg.Cache.Redis.Key)
return redisCache, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the actual file to verify the code at lines 139-158
if [ -f "internal/providers/init.go" ]; then
  wc -l internal/providers/init.go
fi

Repository: ENTERPILOT/GOModel

Length of output: 92


🏁 Script executed:

# Read lines around 139-158 to verify the actual code
if [ -f "internal/providers/init.go" ]; then
  sed -n '130,165p' internal/providers/init.go
fi

Repository: ENTERPILOT/GOModel

Length of output: 964


🏁 Script executed:

# Search for all redis URL logging in the codebase
rg "redis" -i -B 2 -A 2 | head -100

Repository: ENTERPILOT/GOModel

Length of output: 4482


🏁 Script executed:

# Check go.mod to see the Go version requirement
head -5 go.mod

Repository: ENTERPILOT/GOModel

Length of output: 98


🏁 Script executed:

# Search for other instances of logging URLs or sensitive data
rg "slog\." internal/providers/ -B 1 -A 1

Repository: ENTERPILOT/GOModel

Length of output: 5850


🌐 Web query:

Go url.Redacted() method availability Go 1.18 1.19 1.20 1.21

💡 Result:

Yes — url.URL.Redacted was added in Go 1.15, so it is available in Go 1.18, 1.19, 1.20 and 1.21. [1][2]

Sources:

  • Go 1.15 release notes (net/url: new URL.Redacted). [1]
  • net/url package docs (URL.Redacted — added in go1.15). [2]

Avoid logging Redis URLs with credentials.

cfg.Cache.Redis.URL may embed passwords; logging it risks secret exposure. Redact before logging using Go's standard url.Redacted() method.

Suggested redaction
 import (
 	"context"
 	"fmt"
 	"log/slog"
+	"net/url"
 	"os"
 	"path/filepath"
 	"sort"
 	"time"
@@
-		slog.Info("using redis cache", "url", cfg.Cache.Redis.URL, "key", cfg.Cache.Redis.Key)
+		redactedURL := cfg.Cache.Redis.URL
+		if u, err := url.Parse(cfg.Cache.Redis.URL); err == nil {
+			redactedURL = u.Redacted()
+		}
+		slog.Info("using redis cache", "url", redactedURL, "key", cfg.Cache.Redis.Key)
 		return redisCache, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch cacheType {
case "redis":
ttl := time.Duration(cfg.Cache.Redis.TTL) * time.Second
if ttl == 0 {
ttl = cache.DefaultRedisTTL
}
redisCfg := cache.RedisConfig{
URL: cfg.Cache.Redis.URL,
Key: cfg.Cache.Redis.Key,
TTL: ttl,
}
redisCache, err := cache.NewRedisCache(redisCfg)
if err != nil {
return nil, err
}
slog.Info("using redis cache", "url", cfg.Cache.Redis.URL, "key", cfg.Cache.Redis.Key)
return redisCache, nil
switch cacheType {
case "redis":
ttl := time.Duration(cfg.Cache.Redis.TTL) * time.Second
if ttl == 0 {
ttl = cache.DefaultRedisTTL
}
redisCfg := cache.RedisConfig{
URL: cfg.Cache.Redis.URL,
Key: cfg.Cache.Redis.Key,
TTL: ttl,
}
redisCache, err := cache.NewRedisCache(redisCfg)
if err != nil {
return nil, err
}
redactedURL := cfg.Cache.Redis.URL
if u, err := url.Parse(cfg.Cache.Redis.URL); err == nil {
redactedURL = u.Redacted()
}
slog.Info("using redis cache", "url", redactedURL, "key", cfg.Cache.Redis.Key)
return redisCache, nil
🤖 Prompt for AI Agents
In `@internal/providers/init.go` around lines 139 - 158, The slog.Info call in the
cacheType "redis" branch is logging cfg.Cache.Redis.URL which may contain
credentials; change the logging to parse and redact the URL via net/url's Parse
and Redacted() (e.g., parse cfg.Cache.Redis.URL to redisURL and use
redisURL.Redacted()), fall back to a safe placeholder if parsing fails, and
update the slog.Info invocation (the switch on cacheType, the
cfg.Cache.Redis.URL usage, and the slog.Info("using redis cache", ...) near the
cache.NewRedisCache call) to log only the redacted URL and key.

Comment on lines +344 to +356
// GetProviderType returns the provider type string for the given model ID.
// Returns empty string if the model is not found.
func (r *ModelRegistry) GetProviderType(modelID string) string {
r.mu.RLock()
defer r.mu.RUnlock()

info, ok := r.models[modelID]
if !ok {
return ""
}

return r.providerTypes[info.Provider]
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how providers are registered in the codebase
echo "=== Checking RegisterProviderWithType usage ==="
rg -n "RegisterProviderWithType" --type go

echo ""
echo "=== Checking RegisterProvider usage (without type) ==="
rg -n "\.RegisterProvider\(" --type go -g '!*_test.go'

echo ""
echo "=== Check the registry.go file for RegisterProvider and RegisterProviderWithType definitions ==="
rg -n "func.*RegisterProvider" --type go internal/providers/registry.go -A 5

Repository: ENTERPILOT/GOModel

Length of output: 2646


Inconsistent provider registration pattern - needs resolution

The method is thread-safe with correct RLock usage, but there's a design issue: main providers (openai, anthropic, gemini, groq, xai) register via RegisterProvider in their init() functions, while RegisterProviderWithType is only used in init.go:188 and tests. This means GetProviderType will return empty strings for models from the five production providers, since their provider objects are never added to the r.providerTypes map.

Either standardize all providers to use RegisterProviderWithType, or update the docstring to clarify that GetProviderType returns empty string for providers registered without type information.

🤖 Prompt for AI Agents
In `@internal/providers/registry.go` around lines 344 - 356, GetProviderType
returns empty for many production providers because those providers are
registered via RegisterProvider but r.providerTypes is only populated by
RegisterProviderWithType; update the registration path so providerTypes is
always populated: modify RegisterProvider (the function that production
providers call in init()) to also set r.providerTypes[provider.Name()] (or call
RegisterProviderWithType internally) so the providerTypes map contains an entry
for every registered provider, ensuring GetProviderType (on ModelRegistry) can
return the correct type.

Comment on lines +117 to +121
// GetProviderType returns the provider type string for the given model.
// Returns empty string if the model is not found.
func (r *Router) GetProviderType(model string) string {
return r.registry.GetProviderType(model)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding checkReady() for consistency with other Router methods.

Unlike other Router methods (ChatCompletion, StreamChatCompletion, Responses, etc.) that call checkReady() before accessing the registry, GetProviderType directly delegates without this check. While returning an empty string for an uninitialized registry may be acceptable, it creates an inconsistency where callers cannot distinguish between "model not found" and "registry not initialized."

Per coding guidelines, the registry should be initialized before routing requests.

♻️ Optional: Add consistency check
 // GetProviderType returns the provider type string for the given model.
 // Returns empty string if the model is not found.
 func (r *Router) GetProviderType(model string) string {
+	if r.registry.ModelCount() == 0 {
+		return ""
+	}
 	return r.registry.GetProviderType(model)
 }

Alternatively, if distinguishing error cases is important, consider returning (string, error) to align with other Router methods.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetProviderType returns the provider type string for the given model.
// Returns empty string if the model is not found.
func (r *Router) GetProviderType(model string) string {
return r.registry.GetProviderType(model)
}
// GetProviderType returns the provider type string for the given model.
// Returns empty string if the model is not found.
func (r *Router) GetProviderType(model string) string {
if r.registry.ModelCount() == 0 {
return ""
}
return r.registry.GetProviderType(model)
}
🤖 Prompt for AI Agents
In `@internal/providers/router.go` around lines 117 - 121, GetProviderType
currently delegates straight to r.registry.GetProviderType without calling
checkReady(), causing inconsistency with other Router methods; update
Router.GetProviderType to call r.checkReady() (or return an error if checkReady
fails) before accessing r.registry so the registry is guaranteed initialized and
callers can distinguish "not initialized" from "model not found"; reference the
Router type, GetProviderType method, r.checkReady(), and
r.registry.GetProviderType when making the change.

Comment on lines +26 to +31
MasterKey string // Optional: Master key for authentication
MetricsEnabled bool // Whether to expose Prometheus metrics endpoint
MetricsEndpoint string // HTTP path for metrics endpoint (default: /metrics)
BodySizeLimit string // Max request body size (e.g., "10M", "1024K")
AuditLogger auditlog.LoggerInterface // Optional: Audit logger for request/response logging
LogOnlyModelInteractions bool // Only log AI model endpoints (default: true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

LogOnlyModelInteractions “default: true” doesn’t match behavior.

Zero value is false, and cfg == nil yields unfiltered logging. Either set the default in New or update the comment to avoid misleading config docs.

✅ Possible defaulting fix
-    // Request logger with optional filtering for model-only interactions
-    if cfg != nil && cfg.LogOnlyModelInteractions {
+    // Request logger with optional filtering for model-only interactions
+    logOnly := true
+    if cfg != nil {
+        logOnly = cfg.LogOnlyModelInteractions
+    }
+    if logOnly {
         e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
             Skipper: func(c echo.Context) bool {
                 return !auditlog.IsModelInteractionPath(c.Request().URL.Path)
             },
             ...
         }))
     } else {
         e.Use(middleware.RequestLogger())
     }
🤖 Prompt for AI Agents
In `@internal/server/http.go` around lines 26 - 31, The struct field
LogOnlyModelInteractions is documented as "default: true" but its zero value is
false and code paths where cfg == nil lead to unfiltered logging; fix by setting
LogOnlyModelInteractions to true during server construction (e.g., in New or
NewServer where the config is initialized) so the default behavior matches the
comment, or alternatively update the comment on LogOnlyModelInteractions to
remove "default: true" to accurately reflect current behavior—modify the
constructor function (New) to initialize cfg.LogOnlyModelInteractions = true
when unspecified, and/or update the field comment to reflect the actual default.

@SantiagoDePolonia SantiagoDePolonia merged commit e429eb0 into main Jan 16, 2026
7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 9, 2026
@SantiagoDePolonia SantiagoDePolonia deleted the feature/logs branch March 22, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants