Skip to content

feat(monitor): add Prometheus metrics - #4077

Open
wzxjohn wants to merge 2 commits into
QuantumNous:mainfrom
wzxjohn:dev
Open

feat(monitor): add Prometheus metrics#4077
wzxjohn wants to merge 2 commits into
QuantumNous:mainfrom
wzxjohn:dev

Conversation

@wzxjohn

@wzxjohn wzxjohn commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

通过三级分层添加 Prometheus 指标,以控制 label 的基数。指标通过专用的内部端口(/metrics)提供,与主 API 服务器相互独立。

Adds comprehensive Prometheus metrics to the API gateway with a three-tiered approach to control label cardinality. Metrics are served on a dedicated internal port (/metrics), separate from the main API server.

🔗 关联任务 / Related Issue

Metric Tiers

Tier 1 — System-Level (Low Cardinality)

Metric Type Labels
newapi_http_requests_total Counter method, path, status_code
newapi_http_request_duration_seconds Histogram method, path, status_code
newapi_http_active_connections Gauge
newapi_redis_commands_total Counter command, status
newapi_redis_command_duration_seconds Histogram command
newapi_db_queries_total Counter operation, status
newapi_db_query_duration_seconds Histogram operation
newapi_db_open_connections Gauge state
newapi_db_wait_total Counter
newapi_db_wait_duration_seconds_total Counter

Tier 2 — Relay/Channel-Level (Moderate Cardinality)

Metric Type Labels
newapi_relay_requests_total Counter channel_id, channel_type, model, status_code, relay_mode
newapi_relay_request_duration_seconds Histogram channel_id, channel_type, model, relay_mode
newapi_relay_first_token_seconds Histogram channel_id, channel_type, model
newapi_relay_tokens_used_total Counter channel_id, channel_type, model, direction
newapi_relay_errors_total Counter channel_id, channel_type, error_type
newapi_relay_retries_total Counter channel_id, channel_type

Tier 3 — User/Token-Level (High Cardinality, Counters Only)

Metric Type Labels
newapi_token_requests_total Counter user_id, token_id
newapi_token_tokens_used_total Counter user_id, token_id, model, direction
newapi_token_quota_consumed_total Counter user_id, token_id

Architecture

New monitor/ package with 7 files:

File Responsibility
metrics.go All Prometheus metric definitions, InitMetrics(), Enabled()
server.go Dedicated HTTP server for /metrics endpoint
middleware.go Gin middleware for HTTP request metrics
redis_hook.go go-redis/v8 Hook for command-level Redis metrics
db.go GORM callbacks for query metrics + background DB pool stats collector
relay.go Relay/channel metric recording helpers
token.go User/token metric recording helpers

Integration Points

  • main.go: Initialize metrics early in startup, register Gin middleware, attach Redis hook after client init
  • model/main.go: Register GORM callbacks and start DB pool stats collector after DB init
  • controller/relay.go: Record relay request/retry/error metrics on success and failure paths
  • service/text_quota.go: Record token usage and quota consumption after billing calculation

Configuration

Env Var Default Description
PROMETHEUS_ENABLED true Set to "false" to fully disable (zero overhead)
PROMETHEUS_PORT 9090 Port for the dedicated metrics HTTP server

Design Decisions

  • Dedicated port: Metrics endpoint is isolated from the public API, avoiding accidental exposure
  • Tiered cardinality: Tier 3 (user/token) uses counters only (no histograms) to limit storage impact at scale
  • Path normalization: HTTP middleware uses c.FullPath() (Gin route patterns) to avoid cardinality explosion from path parameters
  • No import cycles: monitor/ uses stdlib log for its own logging instead of importing common
  • Fire-and-forget: All metric recording is non-blocking; Enabled() guard on every call path

Summary by CodeRabbit

  • New Features
    • Added comprehensive Prometheus observability: HTTP middleware, metrics HTTP endpoint, and conditional initialization.
    • Instrumented database queries and connection pool collection.
    • Added Redis command metrics.
    • Added relay metrics (requests, durations, first-token, retries, errors) and token usage metrics.
    • Tier‑3 high‑cardinality user/token metrics gated by config to control privacy/cost.

@coderabbitai

coderabbitai Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a Prometheus-based monitoring package and instruments HTTP middleware, GORM DB callbacks and pool, Redis hooks, relay/token flows, and startup metrics server; promotes Prometheus client to a direct dependency and wires metric recording into controller and service logic.

Changes

Cohort / File(s) Summary
Monitor core
monitor/metrics.go, monitor/server.go
New Prometheus metric definitions, InitMetrics guard, Tier‑3 opt‑in, and StartMetricsServer exposing /metrics.
Monitor instrumentation
monitor/middleware.go, monitor/db.go, monitor/redis_hook.go
Gin middleware for HTTP metrics, GORM callbacks and DB pool collector, and Redis client hook for command metrics.
Relay & token helpers
monitor/relay.go, monitor/token.go
Relay metrics struct and recorders (requests, retries, tokens) and Tier‑3 token request/usage functions.
App wiring
main.go, model/main.go
Initialize metrics at startup, add Prometheus middleware, register DB callbacks and Redis hook conditionally, and optionally start metrics server.
Controller changes
controller/relay.go
Added monitor.RecordRelayRequest/RecordRelayRetry/RecordRelayTokens and token usage recording on success/failure/retry paths.
Service changes
service/text_quota.go
Calls monitor.RecordRelayTokens and monitor.RecordTokenUsage after quota summary calculation.
Dependency
go.mod
Prometheus client moved from indirect to direct dependency (github.com/prometheus/client_golang v1.22.0).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Gin as "Gin Server\n(HTTP + Middleware)"
    participant Handler as "App Handler\n(controller/relay.go)"
    participant DB as "GORM / SQL DB"
    participant Redis as "Redis Client"
    participant Monitor as "monitor package"
    participant Metrics as "Prometheus\n/metrics HTTP"

    Client->>Gin: HTTP request
    Gin->>Monitor: PrometheusMiddleware (start)
    Gin->>Handler: invoke handler
    Handler->>DB: run queries
    DB-->>Monitor: GORM callbacks record DB metrics
    Handler->>Redis: execute commands
    Redis-->>Monitor: Redis hook records command metrics
    Handler->>Monitor: RecordRelayRequest / RecordRelayRetry / RecordRelayTokens
    Monitor-->>Metrics: expose metrics via /metrics endpoint
    Handler-->>Gin: response
    Gin->>Monitor: PrometheusMiddleware (observe) 
    Gin-->>Client: HTTP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024

Poem

🐰 I hopped through code with tiny feet,
I stitched each metric, neat and sweet.
From HTTP paths to DB pools,
Redis pings and relay rules—
I count the tokens, drum the beat. 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(monitor): add Prometheus metrics' clearly and concisely summarizes the main change: introducing a new monitoring package with Prometheus metrics integration.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
monitor/metrics.go (2)

11-14: Potential data race on enabled flag.

The enabled variable is written inside initOnce.Do() and read by Enabled() without synchronization. While sync.Once guarantees the write happens-before any subsequent Do() call returns, concurrent readers calling Enabled() before InitMetrics() completes could observe a stale false value, which is likely the intended behavior. However, if Enabled() is called concurrently during InitMetrics() execution, there's a potential race.

Consider using sync/atomic for the flag to ensure memory visibility:

🔧 Proposed fix using atomic
 import (
 	"sync"
+	"sync/atomic"

 	"github.com/prometheus/client_golang/prometheus"
 )

 const namespace = "newapi"

 var (
 	initOnce sync.Once
-	enabled  bool
+	enabled  atomic.Bool
 )

Then update Enabled():

 func Enabled() bool {
-	return enabled
+	return enabled.Load()
 }

And in InitMetrics():

-		enabled = true
+		enabled.Store(true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/metrics.go` around lines 11 - 14, The package has a potential data
race on the package-level enabled flag: change the `enabled` variable to an
atomic type (e.g., uint32) and use atomic operations so reads/writes are
synchronized; update `Enabled()` to return atomic.LoadUint32(&enabled) == 1 and
set the flag in `InitMetrics()` with atomic.StoreUint32(&enabled, 1) (or 0 on
failure) instead of unsynchronized reads/writes, leaving `initOnce` usage intact
to ensure one-time initialization.

158-185: High cardinality metrics may impact Prometheus performance.

Tier 3 metrics use user_id and token_id labels, which can create unbounded cardinality (O(users × tokens × models)). While the design decision to use only counters (no histograms) is appropriate, consider:

  1. Adding documentation about expected cardinality and Prometheus resource requirements
  2. Providing a configuration option to disable Tier 3 metrics independently
  3. Implementing metric expiration/cleanup for inactive users/tokens
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/metrics.go` around lines 158 - 185, Tier 3 counters
(TokenRequestsTotal, TokenTokensUsedTotal, TokenQuotaConsumedTotal) create
potentially unbounded label cardinality (user_id, token_id, model) which can
hurt Prometheus; add a runtime toggle to enable/disable these Tier 3 metrics
(e.g., a config flag or env var read during metrics init), document expected
cardinality and resource guidance for running with this flag enabled, and
implement a cleanup/expiration strategy for these CounterVecs (e.g.,
periodically calling DeleteLabelValues or using a short-lived registry) when
tokens/users become inactive so labels are pruned; update the metrics
initialization code to respect the new config flag and add brief docs mentioning
Prometheus requirements and recommended limits.
monitor/db.go (2)

78-87: Background goroutine lacks shutdown mechanism.

The StartDBPoolCollector goroutine runs indefinitely with no way to stop it. This can cause issues during graceful shutdown or in tests. Consider accepting a context:

🔧 Proposed enhancement for graceful shutdown
-func StartDBPoolCollector(sqlDB *sql.DB) {
+func StartDBPoolCollector(ctx context.Context, sqlDB *sql.DB) {
 	if sqlDB == nil || !Enabled() {
 		return
 	}

 	go func() {
 		ticker := time.NewTicker(15 * time.Second)
 		defer ticker.Stop()
-		for range ticker.C {
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+			}
 			stats := sqlDB.Stats()
 			DBOpenConnections.WithLabelValues("open").Set(float64(stats.OpenConnections))
 			DBOpenConnections.WithLabelValues("in_use").Set(float64(stats.InUse))
 			DBOpenConnections.WithLabelValues("idle").Set(float64(stats.Idle))
 		}
 	}()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/db.go` around lines 78 - 87, The background goroutine that polls
sqlDB.Stats() (currently started in StartDBPoolCollector) runs forever and needs
a shutdown mechanism; modify StartDBPoolCollector to accept a context.Context,
replace the for range ticker.C loop with a select that listens for ctx.Done()
and the ticker.C case, and on ctx cancellation stop the ticker and return;
update references to sqlDB.Stats(), DBOpenConnections and the ticker usage
within that function accordingly and ensure callers pass a cancellable or app
context so the collector can be cleanly stopped.

36-52: Callback registration errors are silently ignored.

The db.Callback().*.Register() calls return errors that are discarded with _. If registration fails (e.g., duplicate callback name), metrics will silently stop working. Consider logging registration failures:

🔧 Proposed fix to log registration errors
+import "log"

 func registerCallback(db *gorm.DB, operation string) {
 	beforeName := "prometheus:before_" + operation
 	afterName := "prometheus:after_" + operation
 	// ... callback definitions ...

 	switch operation {
 	case "create":
-		_ = db.Callback().Create().Before("*").Register(beforeName, before)
-		_ = db.Callback().Create().After("*").Register(afterName, after)
+		if err := db.Callback().Create().Before("*").Register(beforeName, before); err != nil {
+			log.Printf("Failed to register DB callback %s: %v", beforeName, err)
+		}
+		if err := db.Callback().Create().After("*").Register(afterName, after); err != nil {
+			log.Printf("Failed to register DB callback %s: %v", afterName, err)
+		}
 	// ... similar for other cases ...
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/db.go` around lines 36 - 52, The callback Register calls currently
discard returned errors; update each registration (the
db.Callback().Create()/Query()/Update()/Delete()/Raw().Before("*").Register(beforeName,
before) and .After("*").Register(afterName, after) calls) to capture the error
into a variable and log it if non-nil (include operation, beforeName/afterName
and the error in the log message) instead of using `_ =`. Apply this change for
both the Before and After registrations in all switch cases so failures (e.g.,
duplicate callback names) are surfaced.
monitor/server.go (1)

32-34: Consider adding graceful shutdown support.

The server currently has no shutdown mechanism. If the main application shuts down, the metrics server goroutine will be abandoned. Consider accepting a context for graceful shutdown:

🔧 Proposed enhancement for graceful shutdown
+import "context"

-func StartMetricsServer() {
+func StartMetricsServer(ctx context.Context) {
 	if !Enabled() {
 		return
 	}
 	// ... port setup ...
 
 	server := &http.Server{
 		Addr:    addr,
 		Handler: mux,
 	}
 
+	go func() {
+		<-ctx.Done()
+		server.Shutdown(context.Background())
+	}()
+
 	log.Printf("Prometheus metrics server listening on %s", addr)
-	if err := http.ListenAndServe(addr, mux); err != nil {
+	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 		log.Printf("Prometheus metrics server error: %v", err)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/server.go` around lines 32 - 34, Replace the direct call to
http.ListenAndServe with an http.Server instance and add graceful shutdown using
a context: create server := &http.Server{Addr: addr, Handler: mux}, run
server.ListenAndServe in a goroutine, and in the main routine wait for the
provided context to be cancelled (ctx.Done()) then call
server.Shutdown(shutdownCtx) with a short timeout context to allow in-flight
requests to finish; update any function signature that currently starts the
server to accept a context.Context so you can invoke server.Shutdown when the
app is stopping (referencing http.ListenAndServe, server.ListenAndServe, and
server.Shutdown).
model/main.go (1)

199-201: Consider instrumenting LOG_DB for consistency.

The main DB is instrumented with Prometheus metrics, but InitLogDB() (lines 218-253) does not register callbacks or pool collectors for LOG_DB when it uses a separate database connection. If observability into log database operations is desired, consider adding similar instrumentation there.

The current placement after connection pool configuration is appropriate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/main.go` around lines 199 - 201, InitLogDB currently creates a separate
log GORM connection but doesn't register Prometheus instrumentation; after you
configure the log DB's connection pool in InitLogDB(), call
monitor.RegisterDBCallbacks(logDB) and monitor.StartDBPoolCollector(logSqlDB)
(where logDB is the GORM *gorm.DB returned/assigned in InitLogDB and logSqlDB is
the underlying *sql.DB obtained via logDB.DB()) so the LOG_DB is instrumented
the same way as the main DB.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/relay.go`:
- Line 235: The Tier 3 metric calls are creating unbounded cardinality via
RecordTokenRequest(relayInfo.UserId, relayInfo.TokenId) (and related metrics
TokenRequestsTotal, TokenTokensUsedTotal, TokenQuotaConsumedTotal) — add a
runtime config flag (e.g., EnableTier3Metrics or DisableHighCardinalityMetrics)
in the monitor or config package and check it before invoking
monitor.RecordTokenRequest and the other Tier 3 metric emitters so these metrics
can be disabled per deployment; alternatively implement low-cardinality
bucketing/sampling inside RecordTokenRequest (e.g., hash the userId/tokenId and
map to fixed N buckets or sample 1/N requests) and update the metric
names/labels accordingly to avoid user_id/token_id labels being emitted for
every unique id.

In `@monitor/metrics.go`:
- Around line 87-100: DBWaitTotal and DBWaitDuration are never updated; modify
StartDBPoolCollector to read stats := sqlDB.Stats() each tick and compute deltas
against stored previous values (e.g., lastWaitCount int64 and lastWaitDuration
time.Duration) and call DBWaitTotal.Add(float64(deltaWaitCount)) and
DBWaitDuration.Add(deltaWaitDuration.Seconds()) only when the current values
exceed the previous ones, then update the stored previous variables so counters
reflect incremental WaitCount and WaitDuration from sql.DBStats rather than
repeatedly adding the cumulative totals.

In `@monitor/middleware.go`:
- Around line 18-23: HTTPActiveConnections may not be decremented if a
downstream handler panics because Dec() is called after c.Next(); fix by making
the decrement run in a defer immediately after increment so it always executes:
add defer HTTPActiveConnections.Dec() right after HTTPActiveConnections.Inc(),
and if you compute request duration using the start variable, move that
duration/reporting logic into a deferred closure that captures start (or perform
duration calculation inside the same defer) so c.Next() can still run and any
panics won't leave the gauge inflated; reference HTTPActiveConnections,
c.Next(), and the start variable.

---

Nitpick comments:
In `@model/main.go`:
- Around line 199-201: InitLogDB currently creates a separate log GORM
connection but doesn't register Prometheus instrumentation; after you configure
the log DB's connection pool in InitLogDB(), call
monitor.RegisterDBCallbacks(logDB) and monitor.StartDBPoolCollector(logSqlDB)
(where logDB is the GORM *gorm.DB returned/assigned in InitLogDB and logSqlDB is
the underlying *sql.DB obtained via logDB.DB()) so the LOG_DB is instrumented
the same way as the main DB.

In `@monitor/db.go`:
- Around line 78-87: The background goroutine that polls sqlDB.Stats()
(currently started in StartDBPoolCollector) runs forever and needs a shutdown
mechanism; modify StartDBPoolCollector to accept a context.Context, replace the
for range ticker.C loop with a select that listens for ctx.Done() and the
ticker.C case, and on ctx cancellation stop the ticker and return; update
references to sqlDB.Stats(), DBOpenConnections and the ticker usage within that
function accordingly and ensure callers pass a cancellable or app context so the
collector can be cleanly stopped.
- Around line 36-52: The callback Register calls currently discard returned
errors; update each registration (the
db.Callback().Create()/Query()/Update()/Delete()/Raw().Before("*").Register(beforeName,
before) and .After("*").Register(afterName, after) calls) to capture the error
into a variable and log it if non-nil (include operation, beforeName/afterName
and the error in the log message) instead of using `_ =`. Apply this change for
both the Before and After registrations in all switch cases so failures (e.g.,
duplicate callback names) are surfaced.

In `@monitor/metrics.go`:
- Around line 11-14: The package has a potential data race on the package-level
enabled flag: change the `enabled` variable to an atomic type (e.g., uint32) and
use atomic operations so reads/writes are synchronized; update `Enabled()` to
return atomic.LoadUint32(&enabled) == 1 and set the flag in `InitMetrics()` with
atomic.StoreUint32(&enabled, 1) (or 0 on failure) instead of unsynchronized
reads/writes, leaving `initOnce` usage intact to ensure one-time initialization.
- Around line 158-185: Tier 3 counters (TokenRequestsTotal,
TokenTokensUsedTotal, TokenQuotaConsumedTotal) create potentially unbounded
label cardinality (user_id, token_id, model) which can hurt Prometheus; add a
runtime toggle to enable/disable these Tier 3 metrics (e.g., a config flag or
env var read during metrics init), document expected cardinality and resource
guidance for running with this flag enabled, and implement a cleanup/expiration
strategy for these CounterVecs (e.g., periodically calling DeleteLabelValues or
using a short-lived registry) when tokens/users become inactive so labels are
pruned; update the metrics initialization code to respect the new config flag
and add brief docs mentioning Prometheus requirements and recommended limits.

In `@monitor/server.go`:
- Around line 32-34: Replace the direct call to http.ListenAndServe with an
http.Server instance and add graceful shutdown using a context: create server :=
&http.Server{Addr: addr, Handler: mux}, run server.ListenAndServe in a
goroutine, and in the main routine wait for the provided context to be cancelled
(ctx.Done()) then call server.Shutdown(shutdownCtx) with a short timeout context
to allow in-flight requests to finish; update any function signature that
currently starts the server to accept a context.Context so you can invoke
server.Shutdown when the app is stopping (referencing http.ListenAndServe,
server.ListenAndServe, and server.Shutdown).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 14efaaab-95e5-4c48-8953-7aee20d115ed

📥 Commits

Reviewing files that changed from the base of the PR and between c9611c4 and 8d8cf0a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • controller/relay.go
  • go.mod
  • main.go
  • model/main.go
  • monitor/db.go
  • monitor/metrics.go
  • monitor/middleware.go
  • monitor/redis_hook.go
  • monitor/relay.go
  • monitor/server.go
  • monitor/token.go
  • service/text_quota.go

Comment thread controller/relay.go
Comment thread monitor/metrics.go
Comment thread monitor/middleware.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
monitor/db.go (1)

73-99: Make StartDBPoolCollector idempotent per DB handle.

Calling this multiple times starts duplicate goroutines and can overcount DBWaitTotal/DBWaitDuration (each goroutine tracks deltas independently).

♻️ Suggested direction
+// guard collector startup per *sql.DB to avoid duplicate goroutines/counter inflation
+// (implementation can use sync.Map keyed by DB pointer)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/db.go` around lines 73 - 99, StartDBPoolCollector currently spawns a
new goroutine each call for the same sqlDB which causes duplicate metrics; make
it idempotent per DB handle by tracking which *sql.DB instances have already had
collectors started (e.g. a package-level startedCollectors map[*sql.DB]struct{}
protected by a mutex or a sync.Map) and return early if sqlDB is already
present; keep the existing metric updates (DBOpenConnections, DBWaitTotal,
DBWaitDuration) and ticker logic unchanged, but only start the goroutine when
the sqlDB key is newly added.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@monitor/db.go`:
- Around line 38-51: The GORM callback Register(...) calls (e.g.,
db.Callback().Create().Before("*").Register(beforeName, before),
db.Callback().Query().After(...), db.Callback().Raw().Before(...), etc.)
currently ignore returned errors via `_ =`, which masks failures like duplicate
callback names; replace each `_ =` with proper error handling: capture the error
(if err := db.Callback()...Register(...); err != nil { /* either return err from
the enclosing function or log it via the package logger (e.g.,
monitorLogger.Errorf) and continue */ }), ensuring you surface failures when
registering beforeName/afterName for Create, Query, Update, Delete, and Raw
callbacks instead of discarding them.

---

Nitpick comments:
In `@monitor/db.go`:
- Around line 73-99: StartDBPoolCollector currently spawns a new goroutine each
call for the same sqlDB which causes duplicate metrics; make it idempotent per
DB handle by tracking which *sql.DB instances have already had collectors
started (e.g. a package-level startedCollectors map[*sql.DB]struct{} protected
by a mutex or a sync.Map) and return early if sqlDB is already present; keep the
existing metric updates (DBOpenConnections, DBWaitTotal, DBWaitDuration) and
ticker logic unchanged, but only start the goroutine when the sqlDB key is newly
added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99eec33e-14ae-439c-83b1-f474ce320d09

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8cf0a and a67e0ed.

📒 Files selected for processing (4)
  • monitor/db.go
  • monitor/metrics.go
  • monitor/middleware.go
  • monitor/token.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • monitor/middleware.go
  • monitor/token.go

Comment thread monitor/db.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
monitor/db.go (1)

78-99: Pool collector goroutine has no shutdown signal.

The ticker goroutine runs for the process lifetime with no stop channel or context. In practice this is fine since StartDBPoolCollector is called once from InitDB and the collector is meant to live as long as the process. Flagging only as an optional future-proofing consideration — if InitDB ever becomes re-entrant (e.g., for tests or DB reconnection), this will leak goroutines and double-write gauges. Not a blocker.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/db.go` around lines 78 - 99, The pool-collector goroutine started in
StartDBPoolCollector runs for process lifetime with no shutdown signal; change
StartDBPoolCollector to accept a context (or a stop channel), and inside the
goroutine replace the for range ticker.C loop with a select that listens on
ticker.C and ctx.Done(); on ctx.Done() stop the ticker and return to prevent
goroutine leakage and double-writing metrics (keep the existing sqlDB.Stats()
and metric updates when ticker.C fires). Update callers (e.g., InitDB) to pass a
cancellable context where appropriate so tests or reconnections can cancel the
collector.
monitor/redis_hook.go (2)

42-58: Minor: pipeline per-command duration is not observed.

In AfterProcessPipeline, only an aggregate "PIPELINE" duration is recorded while per-command counters are still incremented. That's a reasonable design choice (you can't separate individual latencies inside a pipeline), but it means RedisCommandDuration will under-count samples relative to RedisCommandsTotal for commands that ever run inside pipelines — worth noting in a dashboard/alert comment or the metric's Help text so consumers don't compute misleading averages by dividing the two.

Also a small nit: cmd.Err() is evaluated twice per command; caching it in a local would be slightly cleaner:

♻️ Optional cleanup
 	for _, cmd := range cmds {
 		command := strings.ToUpper(cmd.Name())
 		status := "success"
-		if cmd.Err() != nil && cmd.Err() != redis.Nil {
+		if err := cmd.Err(); err != nil && err != redis.Nil {
 			status = "error"
 		}
 		RedisCommandsTotal.WithLabelValues(command, status).Inc()
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/redis_hook.go` around lines 42 - 58, AfterProcessPipeline currently
records per-command counts (RedisCommandsTotal in
redisMetricsHook.AfterProcessPipeline) but only records a single aggregate
duration label ("PIPELINE") in RedisCommandDuration, which can mislead consumers
who compute per-command averages; update the metric Help text or add a comment
near redisMetricsHook.AfterProcessPipeline/RedisCommandDuration explaining that
per-command durations are not available for pipeline commands and that counts
may outnumber duration samples, and while here also micro-optimize by caching
cmd.Err() into a local variable inside the loop (use the cached err when
deciding status) to avoid evaluating cmd.Err() twice.

17-19: Optional: short-circuit BeforeProcess/BeforeProcessPipeline when metrics are disabled.

When Enabled() is false, the After* handlers return immediately, but Before* still allocates a new context and calls time.Now() on every Redis command. Since enabled is set once via sync.Once and never changes, you can skip the allocation entirely:

♻️ Proposed refactor
 func (h *redisMetricsHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
+	if !Enabled() {
+		return ctx, nil
+	}
 	return context.WithValue(ctx, redisMetricsHookKey{}, time.Now()), nil
 }
@@
 func (h *redisMetricsHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {
+	if !Enabled() {
+		return ctx, nil
+	}
 	return context.WithValue(ctx, redisMetricsHookKey{}, time.Now()), nil
 }

Note: since RegisterRedisHook already gates on Enabled() at startup and the flag is immutable afterwards, this is purely a minor micro-optimization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monitor/redis_hook.go` around lines 17 - 19, The BeforeProcess and
BeforeProcessPipeline methods (redisMetricsHook.BeforeProcess /
redisMetricsHook.BeforeProcessPipeline) should short-circuit when metrics are
disabled to avoid allocating a new context and calling time.Now() on every
command; detect the enabled flag (the existing Enabled() check or the hook's
configured state) at the start of each Before* method and immediately return
(ctx, nil) if disabled, leaving After* unchanged. This uses the immutability of
the enabled flag (set once via sync.Once) to safely skip the
context.WithValue/time.Now allocation when metrics are off.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/relay.go`:
- Around line 250-263: The failure-path metrics block records
relayInfo.ChannelId/ChannelType even when no channel was selected (e.g.,
getChannel failed in the retry loop), producing misleading series; update the
failure-path before calling monitor.RecordRelayRequest /
monitor.RecordTokenRequest to check whether a channel was actually selected
(inspect relayInfo.ChannelId and relayInfo.ChannelType) and either skip emitting
metrics when they are unset or replace them with a clear sentinel (e.g., -1 or
"none"); ensure this guard is applied in the block that handles newAPIError (the
code that calls monitor.RecordRelayRequest and monitor.RecordTokenRequest) so
only real channel selections are recorded.

---

Nitpick comments:
In `@monitor/db.go`:
- Around line 78-99: The pool-collector goroutine started in
StartDBPoolCollector runs for process lifetime with no shutdown signal; change
StartDBPoolCollector to accept a context (or a stop channel), and inside the
goroutine replace the for range ticker.C loop with a select that listens on
ticker.C and ctx.Done(); on ctx.Done() stop the ticker and return to prevent
goroutine leakage and double-writing metrics (keep the existing sqlDB.Stats()
and metric updates when ticker.C fires). Update callers (e.g., InitDB) to pass a
cancellable context where appropriate so tests or reconnections can cancel the
collector.

In `@monitor/redis_hook.go`:
- Around line 42-58: AfterProcessPipeline currently records per-command counts
(RedisCommandsTotal in redisMetricsHook.AfterProcessPipeline) but only records a
single aggregate duration label ("PIPELINE") in RedisCommandDuration, which can
mislead consumers who compute per-command averages; update the metric Help text
or add a comment near redisMetricsHook.AfterProcessPipeline/RedisCommandDuration
explaining that per-command durations are not available for pipeline commands
and that counts may outnumber duration samples, and while here also
micro-optimize by caching cmd.Err() into a local variable inside the loop (use
the cached err when deciding status) to avoid evaluating cmd.Err() twice.
- Around line 17-19: The BeforeProcess and BeforeProcessPipeline methods
(redisMetricsHook.BeforeProcess / redisMetricsHook.BeforeProcessPipeline) should
short-circuit when metrics are disabled to avoid allocating a new context and
calling time.Now() on every command; detect the enabled flag (the existing
Enabled() check or the hook's configured state) at the start of each Before*
method and immediately return (ctx, nil) if disabled, leaving After* unchanged.
This uses the immutability of the enabled flag (set once via sync.Once) to
safely skip the context.WithValue/time.Now allocation when metrics are off.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10437a1e-fe32-42a0-b89a-9f93f681e7cb

📥 Commits

Reviewing files that changed from the base of the PR and between a67e0ed and 7dce6da.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • controller/relay.go
  • go.mod
  • main.go
  • model/main.go
  • monitor/db.go
  • monitor/metrics.go
  • monitor/middleware.go
  • monitor/redis_hook.go
  • monitor/relay.go
  • monitor/server.go
  • monitor/token.go
  • service/text_quota.go
✅ Files skipped from review due to trivial changes (3)
  • go.mod
  • model/main.go
  • monitor/token.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • service/text_quota.go
  • monitor/server.go
  • main.go
  • monitor/middleware.go
  • monitor/metrics.go

Comment thread controller/relay.go
Comment on lines +250 to 263
// Record metrics for failed relay
if newAPIError != nil {
monitor.RecordRelayRequest(&monitor.RelayMetricsData{
ChannelId: relayInfo.ChannelId,
ChannelType: relayInfo.ChannelType,
Model: relayInfo.OriginModelName,
RelayMode: relayInfo.RelayMode,
StatusCode: newAPIError.StatusCode,
StartTime: relayInfo.StartTime,
IsStream: relayInfo.IsStream,
ErrorType: string(newAPIError.GetErrorCode()),
})
monitor.RecordTokenRequest(relayInfo.UserId, relayInfo.TokenId)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Failure-path metrics can record channel_id=0 / channel_type=0.

If the retry loop breaks at line 196 because getChannel returned an error on the first iteration, relayInfo.ChannelId/ChannelType are never populated and you'll emit a failure series labeled with channel_id="0", channel_type="0". Over time this conflates "no channel available" with legitimate channel 0 and contributes an extra series for every error code seen before channel selection.

Consider skipping the emission (or using a sentinel like -1/"none") when no channel was actually selected, e.g.:

Proposed guard
 	if newAPIError != nil {
+		// Only record channel-scoped metrics if a channel was actually selected.
+		if relayInfo.ChannelId != 0 {
 		monitor.RecordRelayRequest(&monitor.RelayMetricsData{
 			ChannelId:   relayInfo.ChannelId,
 			...
 			ErrorType:   string(newAPIError.GetErrorCode()),
 		})
+		}
 		monitor.RecordTokenRequest(relayInfo.UserId, relayInfo.TokenId)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/relay.go` around lines 250 - 263, The failure-path metrics block
records relayInfo.ChannelId/ChannelType even when no channel was selected (e.g.,
getChannel failed in the retry loop), producing misleading series; update the
failure-path before calling monitor.RecordRelayRequest /
monitor.RecordTokenRequest to check whether a channel was actually selected
(inspect relayInfo.ChannelId and relayInfo.ChannelType) and either skip emitting
metrics when they are unset or replace them with a clear sentinel (e.g., -1 or
"none"); ensure this guard is applied in the block that handles newAPIError (the
code that calls monitor.RecordRelayRequest and monitor.RecordTokenRequest) so
only real channel selections are recorded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

支持metrics,实现prometheus+grafna监控关键指标

1 participant