@@ -177,6 +177,37 @@ func opsInsertErrorLogArgs(input *service.OpsInsertErrorLogInput) []any {
177177 }
178178}
179179
180+ // opsErrorLogsOrderBy builds the ORDER BY clause from a whitelist, mirroring
181+ // usageLogOrderBy semantics. Unknown SortBy falls back to created_at; e.id is
182+ // always appended as tiebreaker for stable pagination.
183+ func opsErrorLogsOrderBy (filter * service.OpsErrorLogFilter ) string {
184+ sortBy := ""
185+ sortOrder := ""
186+ if filter != nil {
187+ sortBy = strings .ToLower (strings .TrimSpace (filter .SortBy ))
188+ sortOrder = strings .ToLower (strings .TrimSpace (filter .SortOrder ))
189+ }
190+
191+ var column string
192+ switch sortBy {
193+ case "model" :
194+ column = "COALESCE(NULLIF(TRIM(e.requested_model), ''), e.model)"
195+ case "status_code" :
196+ // 与展示列/过滤保持同义:列表展示 COALESCE(upstream_status_code, status_code, 0),
197+ // status_code 过滤也用同一表达式,故排序必须一致——否则 recovered upstream 行
198+ //(status_code<400 但展示上游 5xx)排序键与显示值/分页切分不符。
199+ column = "COALESCE(e.upstream_status_code, e.status_code, 0)"
200+ default :
201+ column = "e.created_at"
202+ }
203+
204+ dir := "DESC"
205+ if sortOrder == "asc" {
206+ dir = "ASC"
207+ }
208+ return fmt .Sprintf ("%s %s, e.id %s" , column , dir , dir )
209+ }
210+
180211func (r * opsRepository ) ListErrorLogs (ctx context.Context , filter * service.OpsErrorLogFilter ) (* service.OpsErrorLogList , error ) {
181212 if r == nil || r .db == nil {
182213 return nil , fmt .Errorf ("nil ops repository" )
@@ -233,25 +264,29 @@ SELECT
233264 COALESCE(a.name, ''),
234265 e.group_id,
235266 COALESCE(g.name, ''),
236- CASE WHEN e.client_ip IS NULL THEN NULL ELSE e.client_ip::text END,
267+ CASE WHEN e.client_ip IS NULL THEN NULL ELSE host( e.client_ip) END,
237268 COALESCE(e.request_path, ''),
238269 e.stream,
239270 COALESCE(e.inbound_endpoint, ''),
240271 COALESCE(e.upstream_endpoint, ''),
241272 COALESCE(e.requested_model, ''),
242273 COALESCE(e.upstream_model, ''),
274+ COALESCE(e.user_agent, ''),
243275 e.request_type,
244276 COALESCE(ak.name, ''),
245277 ak.deleted_at,
246- COALESCE(e.deleted_key_name, '')
278+ COALESCE(e.deleted_key_name, ''),
279+ e.deleted_key_owner_user_id,
280+ COALESCE(du.email, '')
247281FROM ops_error_logs e
248282LEFT JOIN accounts a ON e.account_id = a.id
249283LEFT JOIN groups g ON e.group_id = g.id
250284LEFT JOIN users u ON e.user_id = u.id
251285LEFT JOIN users u2 ON e.resolved_by_user_id = u2.id
286+ LEFT JOIN users du ON e.deleted_key_owner_user_id = du.id
252287LEFT JOIN api_keys ak ON ak.id = e.api_key_id
253288` + where + `
254- ORDER BY e.created_at DESC
289+ ORDER BY ` + opsErrorLogsOrderBy ( filter ) + `
255290LIMIT $` + itoa (len (args )+ 1 ) + ` OFFSET $` + itoa (len (args )+ 2 )
256291
257292 rows , err := r .db .QueryContext (ctx , selectSQL , argsWithLimit ... )
@@ -279,6 +314,8 @@ LIMIT $` + itoa(len(args)+1) + ` OFFSET $` + itoa(len(args)+2)
279314 var apiKeyName string
280315 var apiKeyDeletedAt sql.NullTime
281316 var deletedKeyName string
317+ var deletedKeyOwnerID sql.NullInt64
318+ var deletedKeyOwnerEmail string
282319 if err := rows .Scan (
283320 & item .ID ,
284321 & item .CreatedAt ,
@@ -311,10 +348,13 @@ LIMIT $` + itoa(len(args)+1) + ` OFFSET $` + itoa(len(args)+2)
311348 & item .UpstreamEndpoint ,
312349 & item .RequestedModel ,
313350 & item .UpstreamModel ,
351+ & item .UserAgent ,
314352 & requestType ,
315353 & apiKeyName ,
316354 & apiKeyDeletedAt ,
317355 & deletedKeyName ,
356+ & deletedKeyOwnerID ,
357+ & deletedKeyOwnerEmail ,
318358 ); err != nil {
319359 return nil , err
320360 }
@@ -364,6 +404,12 @@ LIMIT $` + itoa(len(args)+1) + ` OFFSET $` + itoa(len(args)+2)
364404 }
365405 // 已删除:ak.deleted_at 非空(软删),或仅命中 deleted_key_name 兜底。
366406 item .APIKeyDeleted = apiKeyDeletedAt .Valid || (apiKeyName == "" && deletedKeyName != "" )
407+ // 已删除 KEY 所有者快照:认证失败行 user_id 为空,列表用户列以此回退。
408+ if deletedKeyOwnerID .Valid {
409+ v := deletedKeyOwnerID .Int64
410+ item .DeletedKeyOwnerUserID = & v
411+ item .DeletedKeyOwnerEmail = deletedKeyOwnerEmail
412+ }
367413 out = append (out , & item )
368414 }
369415 if err := rows .Err (); err != nil {
@@ -417,7 +463,7 @@ SELECT
417463 COALESCE(a.name, ''),
418464 e.group_id,
419465 COALESCE(g.name, ''),
420- CASE WHEN e.client_ip IS NULL THEN NULL ELSE e.client_ip::text END,
466+ CASE WHEN e.client_ip IS NULL THEN NULL ELSE host( e.client_ip) END,
421467 COALESCE(e.request_path, ''),
422468 e.stream,
423469 COALESCE(e.inbound_endpoint, ''),
@@ -927,12 +973,14 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) {
927973 if filter != nil {
928974 resolvedFilter = filter .Resolved
929975 }
930- // Keep list endpoints scoped to client errors unless explicitly filtering upstream phase.
976+ // Keep list endpoints scoped to client errors unless the caller explicitly opts
977+ // into recovered upstream rows (Phase=="upstream" + IncludeRecoveredUpstream,
978+ // ops 专用上游列表)。请求错误语义的端点即便过滤 phase=upstream 也保留该守卫。
931979 // cyber_policy is exempt from the status >= 400 guard: streaming cyber hits arrive with
932980 // status 200 (the SSE stream opened successfully before upstream returned response.failed),
933981 // but they are always client-visible blocked requests that belong in admin + user error
934982 // lists. Without the exemption the entire streaming-path cyber sink would be invisible.
935- if phaseFilter != "upstream" {
983+ if phaseFilter != "upstream" || filter == nil || ! filter . IncludeRecoveredUpstream {
936984 clauses = append (clauses , "(COALESCE(e.status_code, 0) >= 400 OR e.error_type = 'cyber_policy')" )
937985 }
938986
0 commit comments