Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (5)
Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (19)
.env.templatecmd/gomodel/main.goconfig/config.godocker-compose.yamlgo.modinternal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/logger.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gointernal/auditlog/stream_wrapper.gointernal/server/http.gointernal/storage/mongodb.gointernal/storage/postgresql.gointernal/storage/sqlite.gointernal/storage/storage.gotests/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 namegomodelfor all imports and package references
Files:
internal/server/http.gointernal/auditlog/store_sqlite.gointernal/auditlog/auditlog.gotests/e2e/auditlog_test.gointernal/storage/postgresql.gointernal/auditlog/store_mongodb.gointernal/auditlog/middleware.gointernal/storage/sqlite.gointernal/auditlog/auditlog_test.gocmd/gomodel/main.goconfig/config.gointernal/auditlog/store_postgresql.gointernal/storage/storage.gointernal/storage/mongodb.gointernal/auditlog/logger.gointernal/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.gonaming convention for internal and config packages
Files:
tests/e2e/auditlog_test.gointernal/auditlog/auditlog_test.go
tests/e2e/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
E2E tests must use the
-tags=e2ebuild 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.gowith 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: UseViperfor 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.gointernal/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.gogo.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.gointernal/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_BODIESis enabled, and notes that sensitive headers are automatically redacted whenLOGGING_LOG_HEADERSis 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.templatedocumentation.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
NORMALsynchronous 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()andClient()methods provide type-safe access to the underlying MongoDB resources without requiring callers to type-assert frominterface{}. 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/v5for PostgreSQL,mongo-driver/v2for MongoDB, andmodernc.org/sqlitefor 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
AuditLoggerfield uses a well-defined interface type (LoggerInterface) rather than a genericinterface{}, 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
Pingand 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 IGNOREhandles 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 e2etag is properly placed, complying with the coding guidelines requiring E2E tests to use the-tags=e2ebuild 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—:0binds 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
Loggerstruct correctly uses a buffered channel for non-blocking writes and async.WaitGroupfor graceful shutdown coordination.
22-44: LGTM: Proper initialization with sensible defaults.The constructor correctly applies defaults for
BufferSizeandFlushInterval, and properly tracks the background goroutine with the WaitGroup before spawning it.
148-169: LGTM: Clean null object pattern implementation.
NoopLoggercorrectly implementsLoggerInterfacewith no-op methods, returningEnabled: falsefromConfig()to indicate the disabled state.internal/auditlog/store_postgresql.go (3)
13-18: LGTM: Clean struct definition.The
PostgreSQLStorestruct 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
MongoDBStorestruct 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
BulkWriteExceptionto 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
TestRedactHeadersfunction 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
TestSkipLoggingPathsfunction 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
omitemptyfor 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.ReadCloserallows 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.
| // Pool returns the underlying pgxpool.Pool for direct access | ||
| func (s *postgresStorage) Pool() *pgxpool.Pool { | ||
| return s.pool | ||
| } |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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:extractHeadersandextractEchoHeadershave 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 forentry.DatainEnrichEntry.
EnrichEntryaccessesentry.Data.PromptTokens(line 209) without checking ifentry.Datais nil, whileEnrichEntryWithErrorandEnrichEntryWithStreaminclude 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
📒 Files selected for processing (6)
.claude/settings.local.jsoncmd/gomodel/main.gointernal/auditlog/auditlog_test.gointernal/auditlog/logger.gointernal/auditlog/middleware.gointernal/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.gowith 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 namegomodelfor all imports and package references
Files:
cmd/gomodel/main.gointernal/auditlog/middleware.gointernal/storage/postgresql.gointernal/auditlog/auditlog_test.gointernal/auditlog/logger.go
**/*_test.go
📄 CodeRabbit inference engine (CLAUDE.md)
Use unit tests alongside implementation files with
*_test.gonaming 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.gointernal/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
postgresStoragestruct correctly holds a reference to*pgxpool.Poolfor 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) toint32by capping atmath.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
Storageinterface definition. The defensive nil check inClose()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.Mutexto protect concurrent access toentriesandclosedfields. The helper methodsgetEntries()andisClosed()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
TestRedactHeadersfunction 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.
TestLogEntryJSONandTestLogDataWithBodiesproperly verify round-trip JSON marshaling/unmarshaling, including nested structures andjson.RawMessagebody fields.
305-329: LGTM! Path skip logic tests are correct.Tests correctly verify that
/health,/metrics, and/favicon.icopaths (and their subpaths) are skipped, while API paths like/v1/chat/completionsare 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
CreateStreamEntryperforms 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
Writemethod now correctly guards againstnilentry.Data(lines 59-62) before accessingRequestID, preventing potential panics when logging dropped entries.
88-133: LGTM! Batch slice reuse race condition fixed.The
flushLoopnow creates a new slice withmake([]*LogEntry, 0, 100)(lines 104, 111) after each flush instead of reusing the backing array withbatch[:0]. This prevents data corruption whenWriteBatchstores or processes the slice asynchronously.
22-44: LGTM! Logger initialization is well-designed.The constructor properly applies sensible defaults for
BufferSize(1000) andFlushInterval(5s), starts the background flush goroutine with properWaitGrouptracking, and returns a fully initialized logger ready for use.
75-86: LGTM! Clean shutdown implementation.The
Closemethod signals the flush loop via thedonechannel, waits for completion withWaitGroup, then closes the underlying store. This ensures all buffered entries are flushed before shutdown.
152-173: LGTM! NoopLogger and interface abstraction.
NoopLoggerprovides a proper stub implementation returningEnabled: falseinConfig(), allowing callers to check if logging is active. TheLoggerInterfaceabstraction enables clean dependency injection and testing.internal/auditlog/middleware.go (3)
137-154: LGTM! SSE streaming and WebSocket support implemented.The
responseBodyCapturenow implementshttp.Flusher(lines 140-144) for SSE streaming support andhttp.Hijacker(lines 149-154) for WebSocket upgrades. Both properly delegate to the underlyingResponseWriterwhen 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
hashAPIKeyfunction 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
initAuditLoggerfunction properly:
- Uses
LOGGING_STORAGE_TYPEto select backend while using shared connection settings- Applies sensible defaults: SQLite type,
.cache/gomodel.dbpath,gomodeldatabase 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) andFlushInterval(5s) when not specified, matching the defaults inNewLogger.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
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.
internal/auditlog/store_mongodb.go
Outdated
| if retentionDays > 0 { | ||
| ttlSeconds := int32(retentionDays * 24 * 60 * 60) | ||
| indexes = append(indexes, mongo.IndexModel{ | ||
| Keys: bson.D{{Key: "timestamp", Value: 1}}, |
There was a problem hiding this comment.
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.
| Keys: bson.D{{Key: "timestamp", Value: 1}}, | |
| Keys: bson.D{{Key: "timestamp", Value: -1}}, |
internal/auditlog/middleware.go
Outdated
| // Write log entry asynchronously | ||
| logger.Write(entry) |
There was a problem hiding this comment.
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) }
| // Write log entry asynchronously | |
| logger.Write(entry) | |
| // Write log entry asynchronously, skipping streaming requests which are logged elsewhere | |
| if !IsEntryMarkedAsStreaming(c) { | |
| logger.Write(entry) | |
| } |
cmd/gomodel/main.go
Outdated
| defer auditStore.Close() | ||
| defer auditLogger.Close() |
There was a problem hiding this comment.
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().
| defer auditStore.Close() | |
| defer auditLogger.Close() | |
| defer auditLogger.Close() | |
| defer auditStore.Close() |
There was a problem hiding this comment.
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
ContentLengthbefore 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:extractHeadersandextractEchoHeadershave 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 forentry.DatainEnrichEntry.
EnrichEntryaccessesentry.Data.PromptTokensetc. without checking ifentry.Datais nil, whileEnrichEntryWithError(line 225) andEnrichEntryWithStream(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.PromptTokensinternal/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
INSERTstatements. 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
expandEnvVarsfunction overridesLOGGING_ENABLED,LOGGING_STORAGE_TYPE,LOGGING_LOG_BODIES, andLOGGING_LOG_HEADERS, but missesLOGGING_BUFFER_SIZE,LOGGING_FLUSH_INTERVAL, andLOGGING_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
📒 Files selected for processing (5)
cmd/gomodel/main.goconfig/config.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.gointernal/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 namegomodelfor all imports and package references
Files:
internal/auditlog/store_postgresql.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.goconfig/config.gocmd/gomodel/main.go
config/config.go
📄 CodeRabbit inference engine (CLAUDE.md)
config/config.go: UseViperfor 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.gowith 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
MongoDBStorestruct 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
FlushandClosemethods 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{}ormap[string]interface{}. The MongoDB driver'sInsertManyaccepts[]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 timeinternal/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
responseBodyCaptureproperly implements:
Writewith 1MB buffer limit (line 132)Flushfor SSE streaming support (lines 142-146)Hijackfor WebSocket upgrades (lines 151-156)This addresses the previous concern about SSE streaming.
180-192: API key hashing implementation is secure.The
hashAPIKeyfunction 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
writeBatchLargefunction 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
CopyFromin 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.
Flushis correctly a no-op for synchronous PostgreSQL writes.Closeproperly 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 (
FlushIntervalwithout "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, andgetEnvBoolhelper 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
gomodelmodule 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 beforeauditStore.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.ErrServerClosedas graceful shutdown (info log)- Treats other errors as fatal (error log + exit)
This addresses the previous review concern.
258-357:initAuditLoggerfunction 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.
There was a problem hiding this comment.
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() | ||
|
|
There was a problem hiding this comment.
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) }
| // Skip logging for specific paths (e.g., /health, /metrics, /favicon.ico) | |
| if SkipLoggingPaths(req.URL.Path) { | |
| return next(c) | |
| } |
| // 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) |
There was a problem hiding this comment.
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.
| // 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) |
| if cfg.MaxConns > 0 { | ||
| maxConns := cfg.MaxConns | ||
| if maxConns > math.MaxInt32 { | ||
| maxConns = math.MaxInt32 | ||
| } | ||
| poolCfg.MaxConns = int32(maxConns) |
There was a problem hiding this comment.
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).
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), |
There was a problem hiding this comment.
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.
| FlushInterval: getEnvIntOrDefault("LOGGING_FLUSH_INTERVAL", 5), | |
| FlushInterval: getEnvIntOrDefault("LOGGING_FLUSH_INTERVAL", 5), // seconds |
| 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)) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
internal/auditlog/middleware.go
Outdated
| if r.body.Len() < 1024*1024 { | ||
| r.body.Write(b) | ||
| } |
There was a problem hiding this comment.
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.
| 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) | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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) |
| 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, | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| func (s *mongoStorage) Close() error { | ||
| if s.client != nil { | ||
| return s.client.Disconnect(context.Background()) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()andauditlog.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
deferexecutes in LIFO (last-in-first-out) order. The current code registersauditLogger.Close()first (line 207) andauditStore.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:
auditLogger.Close()executes first → flushes buffered entries to storageauditStore.Close()executes second → safely closes the connection
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
.claude/settings.local.jsoncmd/gomodel/main.gointernal/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.gowith 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 namegomodelfor all imports and package references
Files:
cmd/gomodel/main.gointernal/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:*andgolangci-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
PartialWriteErrortype provides good context withTotalEntries,FailedCount, and the underlyingCause. The implementation ofUnwrap()returning the sentinelErrPartialWriteallows callers to useerrors.Is()for easy detection.
105-143: LGTM! Solid batch write implementation with proper error handling.The
WriteBatchmethod correctly:
- Uses unordered inserts for partial success scenarios
- Detects
BulkWriteExceptionand returns a typedPartialWriteError- Increments the Prometheus counter for observability
- Logs partial failures with useful context
The
[]interface{}slice on line 112 is required by the MongoDB driver'sInsertManyAPI 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 ininitAuditLogger.
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 failuresThis 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.StorageTypeto select the backend while using shared connection settings fromcfg.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
LogStorebased 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.
| _, 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) | ||
| } |
There was a problem hiding this comment.
🧹 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).
There was a problem hiding this comment.
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.
internal/auditlog/store_mongodb.go
Outdated
| // 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) |
There was a problem hiding this comment.
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.
| 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 |
internal/auditlog/middleware.go
Outdated
| // 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)) |
There was a problem hiding this comment.
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.
cmd/gomodel/main.go
Outdated
| defer auditLogger.Close() | ||
| defer auditStore.Close() |
There was a problem hiding this comment.
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.
| return func(c echo.Context) error { | ||
| // Skip if logging is disabled | ||
| if logger == nil || !logger.Config().Enabled { | ||
| return next(c) | ||
| } |
There was a problem hiding this comment.
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.
internal/auditlog/stream_wrapper.go
Outdated
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_deltaconversion 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), butEnrichEntryis called withnilusage 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 thestart_periodof 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
expandEnvVarsfunction handles overrides forLOGGING_ENABLED,LOGGING_STORAGE_TYPE,LOGGING_LOG_BODIES,LOGGING_LOG_HEADERS, andLOGGING_ONLY_MODEL_INTERACTIONS, butLOGGING_BUFFER_SIZE,LOGGING_FLUSH_INTERVAL, andLOGGING_RETENTION_DAYSare 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, loggerinternal/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: Usingmap[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
Usagestruct is strongly-typed.
149-185: Shallow copy of pointer fields shares underlying values.
TemperatureandMaxTokenspointer 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 forentry.DatainEnrichEntry.
EnrichEntryaccessesentry.Data.PromptTokenswithout checking ifentry.Datais nil, whileEnrichEntryWithErrorandEnrichEntryWithStreaminclude 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
LogBodiesis 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:extractHeadersandextractEchoHeadersare 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
BufferSizeandFlushIntervalare already applied inauditlog.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 beforeauditLogger.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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (26)
.claude/settings.local.jsoncmd/gomodel/main.goconfig/config.goconfig/config_test.godocker-compose.yamlgo.modinternal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/middleware.gointernal/auditlog/stream_wrapper.gointernal/core/interfaces.gointernal/core/responses.gointernal/core/types.gointernal/providers/anthropic/anthropic.gointernal/providers/gemini/gemini.gointernal/providers/groq/groq.gointernal/providers/groq/groq_test.gointernal/providers/openai/openai.gointernal/providers/registry.gointernal/providers/responses_converter.gointernal/providers/router.gointernal/providers/xai/xai.gointernal/server/handlers.gointernal/server/handlers_test.gointernal/server/http.gotests/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 namegomodelfor all imports and package references
Files:
internal/providers/registry.gointernal/core/responses.gointernal/providers/openai/openai.gotests/e2e/auditlog_test.gointernal/providers/xai/xai.goconfig/config_test.gointernal/providers/responses_converter.gointernal/core/interfaces.gointernal/providers/router.gocmd/gomodel/main.gointernal/providers/groq/groq.gointernal/server/http.gointernal/providers/anthropic/anthropic.gointernal/core/types.gointernal/providers/gemini/gemini.gointernal/server/handlers_test.gointernal/server/handlers.gointernal/providers/groq/groq_test.gointernal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/middleware.gointernal/auditlog/stream_wrapper.goconfig/config.go
internal/providers/{router,registry}.go
📄 CodeRabbit inference engine (CLAUDE.md)
The ModelRegistry must be initialized before routing requests; check
ModelCount() > 0or use router afterInitializeAsync()completes to avoidErrRegistryNotInitializederrors
Files:
internal/providers/registry.gointernal/providers/router.go
internal/providers/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
Streaming responses must return
io.ReadCloserand callers are responsible for closing the stream
Files:
internal/providers/registry.gointernal/providers/openai/openai.gointernal/providers/xai/xai.gointernal/providers/responses_converter.gointernal/providers/router.gointernal/providers/groq/groq.gointernal/providers/anthropic/anthropic.gointernal/providers/gemini/gemini.gointernal/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.gonaming convention for internal and config packages
Files:
tests/e2e/auditlog_test.goconfig/config_test.gointernal/server/handlers_test.gointernal/providers/groq/groq_test.gointernal/auditlog/auditlog_test.go
tests/e2e/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
E2E tests must use the
-tags=e2ebuild 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.gowith 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.gointernal/server/handlers_test.gointernal/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: UseViperfor 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.gocmd/gomodel/main.gointernal/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.gocmd/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.gointernal/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.gointernal/providers/openai/openai.gointernal/providers/xai/xai.gointernal/providers/responses_converter.gointernal/core/interfaces.gointernal/providers/router.gocmd/gomodel/main.gointernal/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.gointernal/core/interfaces.gointernal/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.gointernal/core/responses.gointernal/providers/openai/openai.gotests/e2e/auditlog_test.gointernal/providers/xai/xai.gointernal/providers/responses_converter.gointernal/core/interfaces.gointernal/providers/router.gointernal/providers/groq/groq.gointernal/providers/anthropic/anthropic.gointernal/core/types.gointernal/providers/gemini/gemini.gointernal/server/handlers_test.gointernal/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.gointernal/providers/openai/openai.gointernal/providers/xai/xai.gointernal/providers/responses_converter.gointernal/providers/groq/groq.gointernal/providers/anthropic/anthropic.gointernal/core/types.gointernal/providers/gemini/gemini.gointernal/providers/groq/groq_test.gointernal/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.gointernal/providers/responses_converter.gointernal/providers/groq/groq.gointernal/providers/gemini/gemini.gointernal/providers/groq/groq_test.gointernal/auditlog/middleware.gointernal/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.gogo.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.gointernal/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.gointernal/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.gointernal/server/handlers.gointernal/auditlog/middleware.gogo.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.goconfig/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.jsonfiles 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
ProviderandCreatedfields extendChatResponsewith 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
Providerfield addition inChatResponse, 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 defaultingresp.Modelfrom 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
RLockfor 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
providerfield 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 theNewOpenAIResponsesStreamConverterconstructor have been updated with the newproviderparameter. 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
Providerfield and the defensive fallback for emptyModelaligns with the provider tagging pattern used across other providers (anthropic, gemini, xai).
180-180: Provider propagation is consistent.Correctly propagates the provider field from
ChatResponsetoResponsesResponse, maintaining consistency in the response chain.
232-232: Stream converter call updated correctly.The third parameter
"groq"matches the updatedNewOpenAIResponsesStreamConvertersignature ininternal/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:
brotlifor decompression of brotli-encoded response bodies in audit logging middlewarepgx/v5for PostgreSQL storage backend via pgxpoolmongo-driver/v2for MongoDB backend in audit logging and storagesqlitefor 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 updatedNewOpenAIResponsesStreamConvertersignature. 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()forCreatedis appropriate since Anthropic's API doesn't return a creation timestamp.
357-377: Message delta streaming chunk updated consistently.The
message_deltahandler mirrors thecontent_block_deltapattern 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
mockStorenow includes proper mutex synchronization withgetEntries()andisClosed()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 theWaitForEntriespattern 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, andcompressBrotliuse 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
brotlidependency 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
LogConfigstruct inconfig/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
LogBodiesis 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
OnlyModelInteractionsdefaults totruewhen the environment variable is not set, which matches thegetEnvBoolOrDefault("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 avoidscheckReady()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 forresp.Modelwhen 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
GetProviderTypeinterface 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.LoggerInterfaceallows 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
BodyLimitbut beforeAuthMiddleware. 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 defaultingresp.Modelensures audit logs have complete metadata.
264-264: LGTM - Provider field propagation.Correctly propagates the
Providerfield 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 updatedNewOpenAIResponsesStreamConverter(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
Routertype correctly implements this method by delegating to theModelRegistry, 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 e2etag correctly marks this as an e2e test file per coding guidelines.
26-105: LGTM! Thread-safe mock store implementation.The
mockLogStorecorrectly usessync.Mutexfor thread-safe access and provides proper defensive copies inGetEntries()andGetAPIEntries(). 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 tolocalhostvia the URL construction at line 137.
160-376: LGTM! Comprehensive audit log middleware test coverage.The test cases properly use
core.ChatRequeststrongly-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 frominterface{}is acceptable here for test verification.The assertion to
map[string]interface{}at line 243 is necessary to verify the stored request body content. SinceLogData.RequestBodyis typed asinterface{}(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
OnlyModelInteractionsconfiguration 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.
LogEntryandLogDatastructs have proper JSON and BSON tags for dual storage compatibility. The use ofinterface{}forRequestBody/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
Configstruct is well-documented andDefaultConfig()provides secure defaults (logging disabled, bodies/headers not logged, 30-day retention). TheOnlyModelInteractionsdefault oftrueappropriately 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
OnlyModelInteractionsconfiguration before processing, avoiding unnecessary work for skipped paths.
156-190: LGTM! Proper http.Flusher and http.Hijacker delegation.The
responseBodyCapturewrapper correctly implementsFlush()for SSE streaming andHijack()for WebSocket upgrades by delegating to the underlyingResponseWriter. The 1MB buffer limit prevents memory exhaustion on response capture.
294-338: LGTM! Robust decompression with bomb protection.The
decompressBodyfunction 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 theirinit()registration, as required by coding guidelines.
198-217: LGTM! Proper audit logger initialization with cleanup.The audit logging is correctly initialized with fallback to
NoopLoggerwhen disabled. BothauditLogger.Close()andauditStore.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
StreamLogWrappercorrectly 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 (checksw.closed), correctly parses usage data before logging, and delegates to the underlying stream's Close.
247-260: LGTM! Clear model interaction path detection.The
IsModelInteractionPathfunction correctly identifies AI model endpoints using prefix matching. The paths align with the documented behavior inConfig.OnlyModelInteractions.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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), | ||
| }, |
There was a problem hiding this comment.
🧹 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.
| // 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()) | ||
| } |
There was a problem hiding this comment.
🧹 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:
- Using a custom
LogValuesFuncin both cases for consistent output format - 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.
There was a problem hiding this comment.
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 usesmap[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
📒 Files selected for processing (6)
internal/auditlog/auditlog_test.gointernal/auditlog/stream_wrapper.gointernal/server/handlers.gointernal/server/handlers_test.gointernal/server/http.gotests/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 namegomodelfor all imports and package references
Files:
internal/server/http.gointernal/server/handlers.gotests/e2e/auditlog_test.gointernal/server/handlers_test.gointernal/auditlog/auditlog_test.gointernal/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.gointernal/server/handlers.gointernal/server/handlers_test.go
**/*_test.go
📄 CodeRabbit inference engine (CLAUDE.md)
Use unit tests alongside implementation files with
*_test.gonaming convention for internal and config packages
Files:
tests/e2e/auditlog_test.gointernal/server/handlers_test.gointernal/auditlog/auditlog_test.go
tests/e2e/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
E2E tests must use the
-tags=e2ebuild 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.gotests/e2e/auditlog_test.gointernal/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.gointernal/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.gointernal/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.gointernal/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.ReadCloserand 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
closedflag 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\nfollows SSE specification.
114-151: Usingmap[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
TemperatureandMaxTokenspointer 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
LoggerInterfaceallows for dependency injection and testing flexibility.
39-45: Logger initialization handles nil config safely.The nil check on
cfgbefore accessingAuditLoggerprevents panics.
67-101: Logging format inconsistency between branches.When
LogOnlyModelInteractionsis true, structuredslog.Infooutput 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:
- Mark as streaming to prevent middleware double-logging
- Get stream from provider
- Create stream entry copy
- Wrap stream for logging
- Set response headers
- Capture headers on entry after setting them
- 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.StatusOKassumes 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
nilas 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 likeparseUsageFromSSE,hashAPIKey, anddecompressBody.
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 aWaitForEntrieshelper 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, andcompressBrotliuse 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
mockLogStoreimplementation is well-designed with proper thread-safety usingsync.Mutex, defensive copying inGetEntries(), and useful wait helpers for async testing.
160-376: LGTM!The
TestAuditLogMiddlewaretest suite comprehensively covers basic metadata capture, body/header logging, redaction, disabled logging, and API key hashing. All request payloads correctly use strongly-typedcore.ChatRequeststructs 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.ChatRequestfor the test payload correctly follows coding guidelines.
706-865: LGTM!Comprehensive test coverage for the
OnlyModelInteractionsfeature, 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.
There was a problem hiding this comment.
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.
internal/auditlog/middleware.go
Outdated
| if r.body.Len() < 1024*1024 { | ||
| r.body.Write(b) | ||
| } |
There was a problem hiding this comment.
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.
| if cfg != nil && cfg.AuditLogger != nil && cfg.AuditLogger.Config().Enabled { | ||
| e.Use(auditlog.Middleware(cfg.AuditLogger)) | ||
| } |
There was a problem hiding this comment.
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)) }
There was a problem hiding this comment.
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 ofstopCleanupchannel.If
Close()is called multiple times, closing an already-closed channel will panic. Add protection usingsync.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
INSERTstatements. 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 atomicityinternal/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 * 60could 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.
extractHeadersandextractEchoHeadershave 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
maxDecompressedSizeon 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-stoppedThen 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=admintests/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, loggerinternal/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 inCreateStreamEntry.
TemperatureandMaxTokenspointer 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
📒 Files selected for processing (10)
docker-compose.yamlinternal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/logger.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gointernal/auditlog/stream_wrapper.gotests/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 namegomodelfor all imports and package references
Files:
internal/auditlog/store_sqlite.gointernal/auditlog/store_mongodb.gointernal/auditlog/auditlog.gointernal/auditlog/logger.gointernal/auditlog/store_postgresql.gointernal/auditlog/auditlog_test.gointernal/auditlog/stream_wrapper.gointernal/auditlog/middleware.gotests/e2e/auditlog_test.go
**/*_test.go
📄 CodeRabbit inference engine (CLAUDE.md)
Use unit tests alongside implementation files with
*_test.gonaming convention for internal and config packages
Files:
internal/auditlog/auditlog_test.gotests/e2e/auditlog_test.go
tests/e2e/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
E2E tests must use the
-tags=e2ebuild 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.gotests/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.gointernal/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.gotests/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
LogEntryfields, and handles index creation errors gracefully. The conditional cleanup goroutine start based onretentionDaysis appropriate.
84-144: LGTM!The batch insert implementation is well-structured with proper JSON marshaling, bool-to-int conversion for SQLite, and
INSERT OR IGNOREfor idempotent writes. The use ofinterface{}for SQL parameter values is acceptable here as it's required by thedatabase/sqlAPI.internal/auditlog/store_postgresql.go (3)
20-85: LGTM!The constructor properly validates input, creates an appropriate PostgreSQL schema with
UUID,TIMESTAMPTZ, andJSONBtypes, and includes a GIN index on thedatacolumn 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 NOTHINGclause 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
LogStoreinterface 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 usinginterface{}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
Configstruct andDefaultConfig()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
PartialWriteErrortype properly wrapsmongo.BulkWriteExceptionwith additional context (total entries, failed count). The Prometheus countergomodel_audit_log_partial_write_failures_totalenables operators to monitor data loss. TheUnwrap()method correctly returnsErrPartialWriteforerrors.Ischecks.
117-155: LGTM!The
WriteBatchimplementation properly handles partial write failures by returning aPartialWriteErrorwith 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
Loggerstruct properly uses channels andsync.WaitGroupfor concurrent operation, aligning with the coding guidelines for goroutine-based concurrency. TheNewLoggerconstructor applies sensible defaults forBufferSizeandFlushInterval.
135-150: LGTM!The
flushBatchmethod properly uses a timeout context (30s) and logs errors without propagating them, which is appropriate for a background flush operation.
152-173: LGTM!The
NoopLoggerandLoggerInterfaceprovide 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 forentry.DatainEnrichEntry.
EnrichEntryaccessesentry.PromptTokensetc. directly on the entry (notentry.Data), which is fine. However, the function signature and pattern differs fromEnrichEntryWithErrorwhich does checkentry.Data. Looking at the code more carefully, I see that token fields are now onLogEntrydirectly (not nested inData), so this is actually correct.
156-190: Good implementation ofhttp.Flusherandhttp.Hijackerinterfaces.The
responseBodyCapturewrapper correctly implements bothFlush()andHijack()by delegating to the underlyingResponseWriter. This ensures SSE streaming and WebSocket upgrades work correctly with the audit logging wrapper.
296-340: Well-implemented decompression with compression bomb protection.The
decompressBodyfunction 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
Readmethod 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
Closemethod correctly:
- Uses idempotent close pattern with
w.closedflag- 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
gomodelservice correctly usescondition: service_healthyfor both Redis and PostgreSQL dependencies, ensuring the application only starts after databases are ready.internal/auditlog/auditlog_test.go (4)
216-250: Thread-safemockStoreimplementation.The
mockStorecorrectly usessync.Mutexfor all operations, with safe accessor methodsgetEntries()andisClosed(). 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 e2eas 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
:0to 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.ChatRequestwithcore.Messagestructs instead ofmap[string]interface{}, complying with coding guidelines. The assertions verify all key metadata fields.
705-863: Excellent test coverage forOnlyModelInteractionsfiltering.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
mockLogStoreimplementation 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.
| // 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, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| // 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| // 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.
| // 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 | ||
| } |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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) butMONGODB_URLis 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
📒 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_onif you want to use Adminer to inspect MongoDB (though Adminer has limited MongoDB support).
108-112: LGTM!Volume declarations for
postgres_dataandmongodb_dataproperly persist database data across container restarts.
53-54: No action needed. Thepostgres:18-alpineimage 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.
There was a problem hiding this comment.
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_PASSWORDand 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
int32when 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.
writeBatchSmalllogs errors but always returnsnil, 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 whenOnlyModelInteractionsis disabled.The
OnlyModelInteractionsfilter doesn't cover operational paths like/health,/metrics, and/favicon.icowhen disabled. TheSkipLoggingPathshelper exists instream_wrapper.gobut 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
📒 Files selected for processing (7)
docker-compose.yamlinternal/auditlog/auditlog.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gotests/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 namegomodelfor all imports and package references
Files:
internal/auditlog/store_sqlite.gointernal/auditlog/store_postgresql.gointernal/auditlog/auditlog.gotests/e2e/auditlog_test.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.go
**/*_test.go
📄 CodeRabbit inference engine (CLAUDE.md)
Use unit tests alongside implementation files with
*_test.gonaming 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=e2ebuild 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 usesmodernc.org/sqlite v1.44.0, which setsSQLITE_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
contextKeytype to prevent context key collisions. ThemaxBodyCaptureconstant properly addresses the previous feedback about magic numbers.
191-208: LGTM!Proper implementation of
http.Flusherandhttp.Hijackerinterfaces 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!
EnrichEntrynow correctly accesses token fields directly onLogEntryrather than throughentry.Data, resolving the previous nil-check concern. The nil/type guards are appropriate.
259-302: LGTM!
EnrichEntryWithErrorproperly guards against nilentry.Databefore assignment. ThetoValidUTF8Stringfunction is a good defensive measure for MongoDB BSON compatibility.
304-348: LGTM!Robust decompression implementation with proper safeguards:
- Compression bomb protection via
maxDecompressedSizelimit- 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.
| // 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) | ||
| } |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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
📒 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 namegomodelfor 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
Loggerstruct is well-designed with strongly-typed fields, uses channels for async buffering, and includes theclosedatomic 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 spawningflushLoop.
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 emptyRequestIDis a good defensive practice.Note: The race condition between
Write()andClose()is addressed in a separate comment on theClose()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
NoopLoggercorrectly implementsLoggerInterfacewith no-op behavior for when audit logging is disabled.
177-182: LGTM!The
LoggerInterfaceprovides a clean abstraction enabling polymorphism betweenLoggerandNoopLogger, 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.
| 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() | ||
| } |
There was a problem hiding this comment.
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.
| // 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), | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| // 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
cmd/gomodel/main.gointernal/auditlog/auditlog_test.gointernal/auditlog/factory.gointernal/auditlog/middleware.gointernal/auditlog/store_postgresql.gointernal/providers/init.gotests/e2e/auditlog_test.gotests/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 namegomodelfor all imports and package references
Files:
tests/e2e/main_test.gointernal/auditlog/factory.gointernal/auditlog/middleware.gocmd/gomodel/main.gointernal/providers/init.gointernal/auditlog/auditlog_test.gotests/e2e/auditlog_test.gointernal/auditlog/store_postgresql.go
**/*_test.go
📄 CodeRabbit inference engine (CLAUDE.md)
Use unit tests alongside implementation files with
*_test.gonaming convention for internal and config packages
Files:
tests/e2e/main_test.gointernal/auditlog/auditlog_test.gotests/e2e/auditlog_test.go
tests/e2e/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
E2E tests must use the
-tags=e2ebuild tag and use the in-process mock LLM server without Docker or external dependencies
Files:
tests/e2e/main_test.gotests/e2e/auditlog_test.go
cmd/gomodel/main.go
📄 CodeRabbit inference engine (CLAUDE.md)
Provider packages must be imported in
cmd/gomodel/main.gowith 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.ReadCloserand 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.gotests/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.gotests/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.gointernal/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.gointernal/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.gointernal/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.gointernal/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.gointernal/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.gointernal/providers/init.gointernal/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.gotests/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.gotests/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
brotlilibrary for compression testing, and uses thesyncpackage for thread-safe mock implementations.
19-113: Comprehensive header redaction test coverage.The
TestRedactHeadersfunction 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.
TestLogEntryJSONandTestLogDataWithBodiesproperly verify round-trip JSON serialization and handle nested structures correctly. The use ofmap[string]interface{}for bodies aligns with the flexible storage requirements for different backends.
215-250: Thread-safe mockStore implementation.The
mockStorecorrectly implements mutex protection for concurrent access, addressing the race condition concern from previous reviews. ThegetEntries()andisClosed()helper methods properly acquire locks before reading shared state.
355-431: Thorough SSE usage parsing tests.
TestParseUsageFromSSEcovers 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
TestHashAPIKeyfunction 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 updatedapiKeyHashPrefixLengthconstant.
644-667: Helper compression functions correctly implemented.The
compressGzip,compressDeflate, andcompressBrotlihelpers 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, andTestDecompressBodyEmptyInputthoroughly 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
PostgreSQLStoreproperly includescloseOnce sync.Onceto prevent double-close panics, addressing the concern from previous reviews.
26-88: Well-structured initialization with proper index creation.The
NewPostgreSQLStoreconstructor properly validates the pool, creates the table with appropriate column types, and creates useful indexes. The GIN index on the JSONBdatacolumn 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 usessync.Onceto prevent double-close panics on thestopCleanupchannel, and includes a nil check for safety.
200-239: Well-implemented cleanup loop with proper lifecycle management.The
cleanupLoopandcleanupfunctions 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
Resultstruct properly encapsulates the logger and storage, and theClose()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 ensuresstore.Close()is called ifcreateLogStorefails.
81-110: Reasonable default values in buildStorageConfig.The defaults (SQLite type,
.cache/gomodel.dbpath,gomodelMongoDB database) are sensible for local development.
145-166: Good default handling in buildLoggerConfig.The function properly applies defaults for
BufferSize(1000) andFlushInterval(5 seconds) when values are zero or negative.internal/providers/init.go (5)
16-37: Good InitResult design with safe cleanup.Setting
r.stopRefresh = nilafter calling it prevents double-call issues. TheClose()method properly handles nil cache.
39-51: Sensible default refresh interval.The 5-minute default for
RefreshIntervalaligns with the learnings about provider model registration refreshing asynchronously every 5 minutes.
68-121: Well-structured initialization with proper cleanup on failure.The
InitWithConfigfunction 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
initCachefunction 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) andapiKeyHashPrefixLength(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
OnlyModelInteractionsis enabled and the path is not a model interaction endpoint.
88-108: Request body capture with size limit protection.The check for
req.ContentLength > maxBodyCapturebefore 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()andHijack()methods correctly delegate to the underlyingResponseWriterif it supports those interfaces, addressing the previous SSE streaming concern.
240-261: Missing nil check forentry.Datain EnrichEntry.
EnrichEntrysets token fields onentrydirectly (lines 257-259), not onentry.Data, so this isn't actually an issue. The fieldsPromptTokens,CompletionTokens, andTotalTokensare onLogEntryitself based on usage in the test file.
263-294: EnrichEntryWithError has appropriate nil check for entry.Data.The function correctly checks
entry.Data != nilbefore settingErrorMessage, whileErrorTypeis set on the entry directly.
308-352: Robust decompression with bomb protection.The
decompressBodyfunction 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.Usagestruct ininternal/core/types.gois part of the API response layer with JSON struct tags (json:"prompt_tokens", etc.) for serialization. Theauditlog.Usagestruct ininternal/auditlog/middleware.gois 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.
| // 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()) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the storage package and interface definition
find . -type f -name "*.go" | head -20Repository: ENTERPILOT/GOModel
Length of output: 645
🏁 Script executed:
# Search for the Storage interface definition
rg "type Storage interface" -A 20Repository: ENTERPILOT/GOModel
Length of output: 1369
🏁 Script executed:
# Also search for the PostgreSQLPool and MongoDatabase methods
rg "PostgreSQLPool|MongoDatabase" -B 2 -A 2Repository: 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.Pooland*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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_URLand 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=trueandLOGGING_LOG_HEADERS=trueby default risks capturing credentials, tokens, and PII. This was previously flagged - consider defaulting tofalsewith 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:8image 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
📒 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.
| adminer: | ||
| image: adminer:5 | ||
| ports: | ||
| - "8081:8080" | ||
| depends_on: | ||
| - postgres | ||
| restart: unless-stopped |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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 oninterface{}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 aWaitForEntriespolling 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.
TemperatureandMaxTokenspointer 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:closedflag should be set inClose()before signaling shutdown.The
closedflag is currently set influshLoop(line 123) after receiving onl.done. This creates a race window whereWrite()can checkclosed.Load()(returns false), thenflushLoopsetsclosed=trueand closes the buffer, andWrite()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
WriteBatchfails, 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
📒 Files selected for processing (7)
internal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/constants.gointernal/auditlog/logger.gointernal/auditlog/middleware.gointernal/auditlog/stream_wrapper.gointernal/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 namegomodelfor all imports and package references
Files:
internal/auditlog/constants.gointernal/server/handlers.gointernal/auditlog/logger.gointernal/auditlog/auditlog_test.gointernal/auditlog/auditlog.gointernal/auditlog/stream_wrapper.gointernal/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.gonaming 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.gointernal/server/handlers.gointernal/auditlog/logger.gointernal/auditlog/auditlog_test.gointernal/auditlog/auditlog.gointernal/auditlog/stream_wrapper.gointernal/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.gointernal/server/handlers.gointernal/auditlog/logger.gointernal/auditlog/auditlog_test.gointernal/auditlog/auditlog.gointernal/auditlog/stream_wrapper.gointernal/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.gointernal/auditlog/stream_wrapper.gointernal/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.gointernal/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.gointernal/auditlog/auditlog.gointernal/auditlog/stream_wrapper.gointernal/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
contextKeytype 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
LogStoreinterface 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
redactedHeadersSetprovides O(1) lookup for sensitive headers. The case-insensitive comparison viastrings.ToLoweris 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=truelimits 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.Boolfor theclosedflag andsync.WaitGroupfor 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
NoopLoggerprovides a clean way to disable logging without nil checks throughout the codebase. TheLoggerInterfaceenables 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
LogEntryandLogDatastructs serialize and deserialize correctly, including theinterface{}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
IsModelInteractionPathwith various path patterns andparseUsageFromSSEwith 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,
Streamis 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
hashAPIKeyincluding consistency checks, and thoroughdecompressBodytests 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
streamResponseBuildercleanly separates ChatCompletion and Responses API fields. TheStreamLogWrapperproperly implementsio.ReadCloserwith 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: Usingmap[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 finalUsagestruct remains strongly-typed.
170-255: LGTM! SSE event parsing correctly handles both API formats.The
parseChatCompletionEventandparseResponsesAPIEventmethods properly extract metadata and accumulate content while respecting theMaxContentCapturelimit.
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
copyMapfunction 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
RequestBodyTooBigToHandlefor 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
IsEntryMarkedAsStreamingcheck correctly ensures that streaming requests are only logged byStreamLogWrapper, addressing the previous concern about duplicate logging.
161-200: LGTM on response capture wrapper with SSE/WebSocket support.The
responseBodyCapturecorrectly implements:
http.Flusherdelegation for SSE streaminghttp.Hijackerdelegation for WebSocket upgrades- Truncation tracking with proper flag setting
202-226: LGTM on header extraction and API key hashing.The consolidated
extractHeadersfunction and configurable hash prefix length address the previous review comments.
228-282: LGTM on enrichment functions.The
EnrichEntryfunction now correctly sets token fields directly onLogEntry(notentry.Data), andEnrichEntryWithErrorproperly guards against nilentry.Databefore accessingErrorMessage.
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
deferto 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.
There was a problem hiding this comment.
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 asChatCompletion.Non-streaming
Responsesalso doesn't capture token usage in the audit log. Consider applying the same fix as suggested forChatCompletionif 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
EnrichEntrycall at line 84 passesnilfor usage. For non-streaming responses,resp.Usagecontains token data but is not captured in the audit log.Critical issue: The proposed fix attempts to nil-check
resp.Usageusingif resp.Usage != nil, butChatResponse.Usageis a value type (not a pointer), so this code will not compile. Additionally, the same issue exists in theResponseshandler (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
Responseshandler (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
📒 Files selected for processing (2)
internal/auditlog/store_postgresql.gointernal/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 namegomodelfor all imports and package references
Files:
internal/auditlog/store_postgresql.gointernal/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.gointernal/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.gointernal/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
Handlerstruct correctly integrates the audit logger with proper typing. Usingauditlog.LoggerInterfaceinstead ofinterface{}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
WrapStreamForLoggingfunction 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
ListModelshandler 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 viasync.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 useserrors.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.
| 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) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🧹 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.
| 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.
| // 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)) | ||
| } |
There was a problem hiding this comment.
🧹 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 catRepository: 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 catRepository: 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 goRepository: 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 -5Repository: 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.
| c.Response().WriteHeader(http.StatusOK) | ||
| _, _ = io.Copy(c.Response().Writer, wrappedStream) |
There was a problem hiding this comment.
🧹 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.
…ier to filter for Relational Databases
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
82c5db7 to
f4ebd2d
Compare
There was a problem hiding this comment.
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/ResponseBodyasinterface{}breaks the strongly-typed payload requirement and weakens compile-time checks. Use a typed wrapper (e.g.,LogBodywithjson.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
📒 Files selected for processing (5)
.claude/settings.local.jsoninternal/auditlog/auditlog.gointernal/auditlog/cleanup.gointernal/auditlog/store_postgresql.gointernal/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 namegomodelfor all imports and package references
Files:
internal/auditlog/cleanup.gointernal/auditlog/store_sqlite.gointernal/auditlog/store_postgresql.gointernal/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.gointernal/auditlog/store_sqlite.gointernal/auditlog/store_postgresql.gointernal/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.gointernal/auditlog/store_sqlite.gointernal/auditlog/store_postgresql.gointernal/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.
| "WebFetch(domain:docs.x.ai)", | ||
| "Bash(go build:*)", | ||
| "Bash(golangci-lint run:*)", | ||
| "Bash(make test-e2e:*)", | ||
| "WebSearch", | ||
| "Bash(go doc:*)", | ||
| "Bash(grep:*)" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and explore the .claude directory
git ls-files | grep -E "\.claude|claude" | head -20Repository: 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"
fiRepository: 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 -5Repository: 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 -10Repository: 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.mdRepository: 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 -20Repository: 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.jsonRepository: 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.
There was a problem hiding this comment.
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, nilAs 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.doneandresponse.createdpayloads are API responses; mapping them withmap[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 lettingMONGODB_DATABASEdefine 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, andLOGGING_RETENTION_DAYSare 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_URLcontains hardcoded credentials which is flagged by static analysis (CKV_SECRET_4). This has been noted in previous reviews. Consider using environment variable substitution from.envfile.
26-27: Header/body logging enabled by default poses privacy risk.Enabling
LOGGING_LOG_BODIES=trueandLOGGING_LOG_HEADERS=trueby 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.Middlewareperforms 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.Sleepwaiting for async flush can cause intermittent failures; prior review suggested aWaitForEntrieshelper.Also applies to: 433-494
internal/storage/postgresql.go (2)
31-36: The MaxConns overflow check is overly defensive.The check
maxConns > math.MaxInt32guards 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 ofPool()method.The
Pool()method provides direct typed access to*pgxpool.Poolbut isn't part of theStorageinterface. Consumers would need to type-assert to*postgresStorageto use it. Consider documenting whether consumers should preferPostgreSQLPool()(via interface) or this method.internal/auditlog/store_sqlite.go (2)
98-123: Nil data converts to empty string instead of SQL NULL.When
marshalLogDatareturnsnil(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 ifNewLoggeralready 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 toDisconnectto 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:closedflag must be set inClose()before signaling shutdown.There's a race window between
Write()andClose():
Write()checksl.closed.Load()(returns false)Close()callsclose(l.done)(line 86)flushLoopreceives onl.done, setsl.closed.Store(true)(line 123), then closesl.buffer(line 124)Write()attempts to send on the now-closedl.buffer→ panicMove the
closedflag set toClose()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
handleStreamingResponsehelper properly:
- Executes
streamFnfirst and marks as streaming only after success (addressing the previous race condition)- Sets appropriate SSE headers
- Wraps the stream for audit logging
- Handles cleanup with deferred close
One minor observability gap remains: the
io.Copyerror 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 niltests/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 ofmap[string]interface{}.Prefer decoding into
core.ChatRequestto 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: Avoidinterface{}for logged request/response bodies.Use a typed wrapper (e.g.,
LogBodywithjson.RawMessageor 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
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (40)
.claude/settings.local.json.env.templatecmd/gomodel/main.goconfig/config.goconfig/config_test.godocker-compose.yamlgo.modinternal/auditlog/auditlog.gointernal/auditlog/auditlog_test.gointernal/auditlog/cleanup.gointernal/auditlog/constants.gointernal/auditlog/factory.gointernal/auditlog/logger.gointernal/auditlog/middleware.gointernal/auditlog/store_mongodb.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gointernal/auditlog/stream_wrapper.gointernal/core/interfaces.gointernal/core/responses.gointernal/core/types.gointernal/providers/anthropic/anthropic.gointernal/providers/gemini/gemini.gointernal/providers/groq/groq.gointernal/providers/groq/groq_test.gointernal/providers/init.gointernal/providers/openai/openai.gointernal/providers/registry.gointernal/providers/responses_converter.gointernal/providers/router.gointernal/providers/xai/xai.gointernal/server/handlers.gointernal/server/handlers_test.gointernal/server/http.gointernal/storage/mongodb.gointernal/storage/postgresql.gointernal/storage/sqlite.gointernal/storage/storage.gotests/e2e/auditlog_test.gotests/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 namegomodelfor all imports and package references
Files:
tests/e2e/main_test.gointernal/providers/router.gointernal/core/interfaces.gointernal/auditlog/auditlog_test.gointernal/providers/registry.gointernal/auditlog/cleanup.goconfig/config_test.gointernal/providers/openai/openai.gointernal/storage/postgresql.gointernal/providers/gemini/gemini.gointernal/server/http.gointernal/auditlog/store_postgresql.gointernal/server/handlers_test.gointernal/core/responses.gointernal/providers/groq/groq_test.gointernal/storage/sqlite.gointernal/auditlog/logger.gointernal/auditlog/factory.gointernal/auditlog/store_sqlite.gotests/e2e/auditlog_test.gointernal/auditlog/store_mongodb.gointernal/auditlog/middleware.gointernal/server/handlers.gocmd/gomodel/main.gointernal/providers/xai/xai.gointernal/providers/anthropic/anthropic.goconfig/config.gointernal/providers/groq/groq.gointernal/auditlog/auditlog.gointernal/storage/mongodb.gointernal/storage/storage.gointernal/providers/init.gointernal/providers/responses_converter.gointernal/core/types.gointernal/auditlog/constants.gointernal/auditlog/stream_wrapper.go
**/*_test.go
📄 CodeRabbit inference engine (CLAUDE.md)
Use unit tests alongside implementation files with
*_test.gonaming convention for internal and config packages
Files:
tests/e2e/main_test.gointernal/auditlog/auditlog_test.goconfig/config_test.gointernal/server/handlers_test.gointernal/providers/groq/groq_test.gotests/e2e/auditlog_test.go
tests/e2e/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
E2E tests must use the
-tags=e2ebuild tag and use the in-process mock LLM server without Docker or external dependencies
Files:
tests/e2e/main_test.gotests/e2e/auditlog_test.go
internal/providers/{router,registry}.go
📄 CodeRabbit inference engine (CLAUDE.md)
The ModelRegistry must be initialized before routing requests; check
ModelCount() > 0or use router afterInitializeAsync()completes to avoidErrRegistryNotInitializederrors
Files:
internal/providers/router.gointernal/providers/registry.go
internal/providers/**/*.go
📄 CodeRabbit inference engine (CLAUDE.md)
Streaming responses must return
io.ReadCloserand callers are responsible for closing the stream
Files:
internal/providers/router.gointernal/providers/registry.gointernal/providers/openai/openai.gointernal/providers/gemini/gemini.gointernal/providers/groq/groq_test.gointernal/providers/xai/xai.gointernal/providers/anthropic/anthropic.gointernal/providers/groq/groq.gointernal/providers/init.gointernal/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.gointernal/server/handlers_test.gointernal/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.gowith 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: UseViperfor 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.gotests/e2e/auditlog_test.gogo.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.gointernal/providers/router.gointernal/core/interfaces.gointernal/auditlog/auditlog_test.gointernal/providers/registry.gointernal/auditlog/cleanup.goconfig/config_test.gointernal/providers/openai/openai.gointernal/storage/postgresql.gointernal/providers/gemini/gemini.gointernal/server/http.gointernal/auditlog/store_postgresql.gointernal/server/handlers_test.gointernal/core/responses.gointernal/providers/groq/groq_test.gointernal/storage/sqlite.gointernal/auditlog/logger.gointernal/auditlog/factory.gointernal/auditlog/store_sqlite.gotests/e2e/auditlog_test.gointernal/auditlog/store_mongodb.gointernal/auditlog/middleware.gointernal/server/handlers.gocmd/gomodel/main.gointernal/providers/xai/xai.gointernal/providers/anthropic/anthropic.goconfig/config.gointernal/providers/groq/groq.gointernal/auditlog/auditlog.gointernal/storage/mongodb.gointernal/storage/storage.gointernal/providers/init.gointernal/providers/responses_converter.gointernal/core/types.gointernal/auditlog/constants.gointernal/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.gointernal/core/interfaces.gointernal/providers/registry.gocmd/gomodel/main.gointernal/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.gointernal/providers/registry.gocmd/gomodel/main.gointernal/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.gointernal/core/interfaces.gointernal/providers/registry.gointernal/providers/openai/openai.gointernal/providers/gemini/gemini.gointernal/server/handlers_test.gointernal/core/responses.gointernal/providers/groq/groq_test.gotests/e2e/auditlog_test.gointernal/server/handlers.gointernal/providers/xai/xai.gointernal/providers/anthropic/anthropic.gointernal/providers/groq/groq.gointernal/providers/init.gointernal/providers/responses_converter.gointernal/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.gointernal/core/interfaces.gointernal/auditlog/auditlog_test.gointernal/providers/registry.gointernal/auditlog/cleanup.gointernal/providers/openai/openai.gointernal/storage/postgresql.gointernal/providers/gemini/gemini.gointernal/server/http.gointernal/auditlog/store_postgresql.gointernal/server/handlers_test.gointernal/core/responses.gointernal/providers/groq/groq_test.gointernal/storage/sqlite.gointernal/auditlog/logger.gointernal/auditlog/factory.gointernal/auditlog/store_sqlite.gointernal/auditlog/store_mongodb.gointernal/auditlog/middleware.gointernal/server/handlers.gointernal/providers/xai/xai.gointernal/providers/anthropic/anthropic.gointernal/providers/groq/groq.gointernal/auditlog/auditlog.gointernal/storage/mongodb.gointernal/storage/storage.gointernal/providers/init.gointernal/providers/responses_converter.gointernal/core/types.gointernal/auditlog/constants.gointernal/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.goconfig/config_test.gotests/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.gointernal/providers/registry.gocmd/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.gointernal/auditlog/factory.gotests/e2e/auditlog_test.gointernal/auditlog/middleware.gointernal/auditlog/auditlog.gointernal/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.gocmd/gomodel/main.gointernal/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.gointernal/providers/openai/openai.gointernal/providers/gemini/gemini.gocmd/gomodel/main.gointernal/providers/xai/xai.gointernal/providers/init.gointernal/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.gocmd/gomodel/main.gointernal/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.goconfig/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.gointernal/providers/gemini/gemini.gointernal/core/responses.gointernal/providers/groq/groq_test.gointernal/server/handlers.gointernal/providers/xai/xai.gointernal/providers/anthropic/anthropic.gointernal/providers/groq/groq.gointernal/providers/responses_converter.gointernal/core/types.gointernal/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.gointernal/auditlog/store_postgresql.gointernal/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.gointernal/core/responses.gointernal/providers/groq/groq_test.gointernal/auditlog/middleware.gointernal/server/handlers.gointernal/providers/groq/groq.gointernal/providers/responses_converter.gointernal/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.gogo.modinternal/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.goconfig/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.
| 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 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| // 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, | ||
| } |
There was a problem hiding this comment.
🧩 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 goRepository: ENTERPILOT/GOModel
Length of output: 1424
🏁 Script executed:
# Check the PartialWriteError struct definition
rg "type PartialWriteError" -A 5 --type goRepository: 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 2Repository: 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 1Repository: 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.
| 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 |
There was a problem hiding this comment.
🧩 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
fiRepository: 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
fiRepository: 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 -100Repository: ENTERPILOT/GOModel
Length of output: 4482
🏁 Script executed:
# Check go.mod to see the Go version requirement
head -5 go.modRepository: 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 1Repository: 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.
| 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.
| // 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] | ||
| } |
There was a problem hiding this comment.
🧩 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 5Repository: 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.
| // 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) | ||
| } |
There was a problem hiding this comment.
🧹 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.
| // 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.
| 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) |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Configuration
Chores
Tests
✏️ Tip: You can customize this high-level summary in your review settings.