mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
+11
@@ -310,6 +310,17 @@ Validation:
|
||||
|
||||
Returns cursor-paginated event logs for admin troubleshooting. The list uses the standard admin page envelope and lower camelCase fields.
|
||||
|
||||
Supported query filters:
|
||||
|
||||
- `kind`
|
||||
- `level`
|
||||
- `requestId`
|
||||
- `accountId`
|
||||
- `route`
|
||||
- `model`
|
||||
- `statusCode`
|
||||
- `search` across message and metadata text
|
||||
|
||||
Example item:
|
||||
|
||||
```json
|
||||
|
||||
@@ -11,7 +11,7 @@ Update this file before each feature commit.
|
||||
| SQLite storage | Completed | `ffec8b5` + `b0c10f2` | `cargo test sqlite_schema_creates_accounts_and_event_tables`; `cargo test connect_sqlite_creates_parent_directory_for_file_database`; full `fmt/test/clippy` before commit | Creates SQLite WAL connector, parent directory bootstrap, sqlx-managed schema for accounts/API keys/sessions/cookies/fingerprints/events, and event/account indexes. |
|
||||
| Admin auth and client API keys | Partial | `24d5799` + `4e0d1e5` + `e9fdea0` + `8af350d` + `960bbb2` | `cargo test client_api_key_has_proxy_prefix_and_verifies_against_hash`; `cargo test admin_password_hash_is_not_a_client_api_key`; `cargo test --test admin_login_route_test`; `cargo test --test admin_api_keys_route_test`; `cargo fmt --check`; `cargo test`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; full `fmt/test/clippy` before `8af350d`; full `fmt/test/clippy` before `960bbb2`; current local key import/export targeted `cargo test --test admin_api_keys_route_test` | Admin passwords use Argon2id, client API keys use `cpr_` prefix plus HMAC-SHA256 with server-side pepper; `/admin/login` issues HttpOnly sessions and rejects client API keys as admin credentials. `/admin/api-keys` can create/list local client keys, status-enable/status-disable local client keys, label local client keys, delete local client keys, batch-delete local client keys, export local client key metadata, import metadata by rotating to new local `cpr_` keys, and `/v1/*` verifies only enabled keys against SQLite instead of accepting any `cpr_` string. These routes are Rust-local client auth utilities, not TypeScript third-party provider API-key parity. Provider API key pools and provider key import/export remain out of scope; local `cpr_` key plaintext and key hashes are not exportable after HMAC storage, and import returns newly generated plaintext values only once. |
|
||||
| Secret encryption | Completed | `105aa58` + `228febd` | `cargo test secret_box_encrypts_and_decrypts_without_storing_plaintext`; `cargo test secret_box_load_or_create_should_reuse_existing_master_key_file`; full `fmt/test/clippy` before commit | AES-256-GCM `SecretBox` stores upstream secrets as `v1:<nonce>:<ciphertext>` without plaintext and can load/create the runtime master key file used by repositories. |
|
||||
| Logging and pagination | Completed | `eb0341e` + `fc16c48` + `b0c10f2` + `ee0ed90` + `fdb9374` + `5bf012c` + `pending` | `cargo test event_logs_are_cursor_paginated`; `cargo test admin_logs_are_cursor_paginated_and_include_request_id`; `cargo test --test admin_logs_route_test admin_logs_state_patch`; `cargo test --test admin_logs_route_test admin_logs_`; `cargo test rotating_writer_splits_files_when_size_limit_is_reached`; `cargo test --test admin_usage_stats_route_test`; `cargo test --test v1_upstream_route_test`; full `fmt/test/clippy` before commit | Adds cursor pagination, `Page<T>` camelCase serialization, admin `/admin/logs` query/detail/state/update/clear APIs, admin `/admin/usage-stats` and `/admin/usage-stats/summary`, event log repository, runtime tracing initialization, HTTP trace layer, date/size-based file rotation, v1 response lifecycle events with request/account/route/model/status/latency/non-secret metadata, and non-secret `account.refresh` events for upstream-401 refresh failures. Runtime log state updates are in-process only; persistent config changes remain under `/admin/settings`. |
|
||||
| Logging and pagination | Completed | `eb0341e` + `fc16c48` + `b0c10f2` + `ee0ed90` + `fdb9374` + `5bf012c` + `pending` | `cargo test event_logs_are_cursor_paginated`; `cargo test admin_logs_are_cursor_paginated_and_include_request_id`; `cargo test --test admin_logs_route_test admin_logs_state_patch`; `cargo test --test admin_logs_route_test admin_logs_should_filter`; `cargo test --test admin_logs_route_test admin_logs_`; `cargo test rotating_writer_splits_files_when_size_limit_is_reached`; `cargo test --test admin_usage_stats_route_test`; `cargo test --test v1_upstream_route_test`; full `fmt/test/clippy` before commit | Adds cursor pagination, `Page<T>` camelCase serialization, admin `/admin/logs` query/detail/state/update/clear APIs, admin `/admin/logs` filters for kind/level/requestId/accountId/route/model/statusCode/search, admin `/admin/usage-stats` and `/admin/usage-stats/summary`, event log repository, runtime tracing initialization, HTTP trace layer, date/size-based file rotation, v1 response lifecycle events with request/account/route/model/status/latency/non-secret metadata, and non-secret `account.refresh` events for upstream-401 refresh failures. Runtime log state updates are in-process only; persistent config changes remain under `/admin/settings`. |
|
||||
| Diagnostics | Completed | `pending` | `cargo test --test diagnostics_route_test admin_diagnostics`; `cargo test --test diagnostics_route_test debug_upstream`; `cargo test --test diagnostics_route_test` | Adds admin-session-gated `GET /admin/diagnostics` plus local-only `GET /debug/diagnostics`, `GET /debug/fingerprint`, and `GET /debug/upstream` with runtime package metadata, config/database/log/security paths, Codex backend transport settings, static Desktop fingerprint summary and standalone fingerprint debug output, account repository/pool state, runtime capacity summary, retained settings summary, and a Codex models endpoint connectivity probe without tokens or upstream bodies. Remote forwarded debug requests are rejected with `403`. |
|
||||
| TLS headers and fingerprint | Completed | `1bdd08c` | `cargo test codex_headers_include_desktop_identity_and_turn_state`; `cargo tree \| rg 'reqwest\|rustls'`; full `fmt/test/clippy` before commit | Adds Codex Desktop fingerprint model, exact identity/request headers, and pinned reqwest/rustls client builder. |
|
||||
| Cookie persistence | Completed | `4710926` + `5d3cc6e` + `203270f` | `cargo test cookie_jar_captures_and_replays_account_scoped_cookies`; `cargo test cookie_repository_encrypts_and_replays_account_scoped_cookies`; full `fmt/test/clippy` before `203270f` | Adds account-scoped in-memory Cookie jar and encrypted SQLite Cookie repository; Cookies are replayed only for the same account/domain. Admin-pasted browser Cookie headers are stored encrypted and can be cleared per account. |
|
||||
|
||||
@@ -2977,7 +2977,7 @@ The Rust project has no legacy compatibility requirement, but the in-scope behav
|
||||
- [x] **Client API key management:** implement admin CRUD for local client API keys: list/create/delete/batch-delete/label/status/export/import. Create/list/status/delete/batch-delete/label/export/import and `/v1/*` SQLite verification are complete. Remove provider/model binding semantics from the TypeScript version; Rust client API keys authorize only local `/v1/*` access.
|
||||
- [x] **Settings:** implement admin settings for server/runtime fields that remain in scope: default model, reasoning effort, service tier, model aliases, refresh enabled/margin/concurrency, max concurrent per account, request interval, rotation strategy, tier priority, quota refresh/thresholds/skip exhausted, logs state/capacity/body capture, and usage history retention. GET and PATCH `/admin/settings` are admin-session gated, PATCH validates retained fields, writes a root `local.yaml` overlay, and rejects proxy URL, Ollama, third-party provider, OpenAI official key, proxy API key, and Electron/self-update settings. Existing constructed runtime services still use construction-time config until restart or a later runtime reconfiguration task.
|
||||
- [x] **Diagnostics:** expand health/admin diagnostics to include authenticated state, pool summary, capacity summary, transport/fingerprint status, paths, runtime metadata, and test-connection checks. `/admin/diagnostics` returns the non-secret runtime summary behind admin-session auth. Local-only `/debug/diagnostics` returns runtime metadata, paths, account repository/pool state, capacity summary, transport config, and static fingerprint status without secrets. Local-only `/debug/fingerprint` returns the runtime Desktop fingerprint summary. Local-only `/debug/upstream` probes the Codex models endpoint with runtime TLS/fingerprint headers and returns only status metadata. Keep production-local gating for sensitive debug endpoints.
|
||||
- [ ] **Logs and usage stats:** add `/admin/logs/state`, clear/detail, error-log equivalents if retained, and richer bounded history queries if needed. `GET/PATCH /admin/logs/state`, `/admin/logs/{id}`, `DELETE /admin/logs`, log file rotation, SQLite event pagination, `/admin/usage-stats` pagination, and `/admin/usage-stats/summary` are complete. Remaining: decide whether to retain old error-log grouping/raw/count/seen/report APIs and richer bounded body-history queries.
|
||||
- [ ] **Logs and usage stats:** add `/admin/logs/state`, clear/detail, error-log equivalents if retained, and richer bounded history queries if needed. `GET/PATCH /admin/logs/state`, `/admin/logs/{id}`, `DELETE /admin/logs`, `/admin/logs` kind/level/request/account/route/model/status/search filters, log file rotation, SQLite event pagination, `/admin/usage-stats` pagination, and `/admin/usage-stats/summary` are complete. Remaining: decide whether to retain old error-log grouping/raw/count/seen/report APIs and richer bounded body-history queries.
|
||||
- [x] **Model catalog:** replace the hard-coded `/v1/models` list with a model store that supports static defaults, configured aliases, `-low/-medium/-high/-xhigh/-fast/-flex` suffix parsing, backend model fetching per plan, cache persistence, `/v1/models/catalog`, `/v1/models/{id}`, `/v1/models/{id}/info`, `/admin/refresh-models`, and `/debug/models`. Static defaults, aliases, suffix parsing, cached backend snapshots, plan allowlists, admin refresh, read-only model routes, and debug summary are complete.
|
||||
- [ ] **Account scheduling parity:** extend account acquisition beyond `active + least last_used`. Completed in the in-memory pool: max concurrent slots per account, stale slot cleanup, least-used/round-robin/sticky strategies, quota-cache skip, tier priority, model-plan filtering, exclude IDs, preferred account from session affinity, Cloudflare cooldown skip, request-staggering metadata, capacity summary, settings-backed construction, import-to-pool sync, startup pool snapshots from SQLite, HTTP fallback acquisition across alternate accounts, WebSocket fallback for non-history requests, previous-response account affinity, and runtime pool clearing on logout. Remaining: durable release-time slot state and durable quota-cache/cooldown restore.
|
||||
- [ ] **Upstream Codex lifecycle:** implement account acquire -> Codex request -> SSE/collect-or-stream -> rate-limit capture -> usage release -> retry/fallback. HTTP SSE collection, streaming passthrough, 401-triggered refresh retry, Codex request context fields, SSE `error`/`response.failed` mapping, 429/402/403 account-state classification, successful WebSocket rate-limit capture, and fallback across alternate accounts for non-streaming Responses, HTTP SSE setup, non-history WebSocket setup, and Chat Completions are complete. Remaining: durable 429 quota cooldown persistence, path-block Cookie clearing, and richer OpenAI-compatible error mapping.
|
||||
@@ -2995,7 +2995,7 @@ This pass re-read the TypeScript routes and core modules under `/home/zyy/Codes/
|
||||
- [ ] **Admin account management:** Rust has paginated sanitized list, native/Sub2API import, native/Sub2API export, manual token add with upstream refresh-token exchange and JWT metadata/expires extraction, device-code token import, label/status/delete, batch delete/status, Cookie get/set/delete, per-account refresh/probe, reset usage, quota fetch/update, quota warnings, auth status, and logout. Missing persistence-health reporting and durable quota-window restore.
|
||||
- [x] **Client API key management:** Rust creates/lists/status-toggles/deletes/batch-deletes/labels/exports/imports local `cpr_` keys and verifies them for `/v1/*`. Do not port the TypeScript provider/model binding or provider model-fetch APIs.
|
||||
- [x] **Settings:** Rust exposes admin-session-gated GET/PATCH `/admin/settings` for retained fields. PATCH validates default model, default reasoning effort, service tier, aliases, refresh settings, max concurrent accounts, request interval, rotation strategy, tier priority, quota refresh/threshold/skip settings, log state/capacity/body capture, and usage retention, writes root `local.yaml`, and does not re-add proxy URL, provider, Ollama, proxy API key, or Electron/self-update settings. Existing constructed runtime services still use construction-time config until restart or a later runtime reconfiguration task.
|
||||
- [ ] **Logs and error diagnostics:** Rust has cursor-paginated SQLite event logs, rotating files, `GET/PATCH /admin/logs/state`, detail-by-id, and clear. Missing direction/search filters, bounded in-memory request/response body capture, and the original local error-log grouping/raw/count/seen/report/clear APIs if retained for troubleshooting.
|
||||
- [ ] **Logs and error diagnostics:** Rust has cursor-paginated SQLite event logs, rotating files, `GET/PATCH /admin/logs/state`, detail-by-id, clear, and admin log filters for kind, level, request ID, account ID, route, model, status code, and message/metadata search. Missing direction/body-history filters, bounded in-memory request/response body capture, and the original local error-log grouping/raw/count/seen/report/clear APIs if retained for troubleshooting.
|
||||
- [ ] **Usage and quota background jobs:** Rust records cumulative account usage from `/v1/*` and computes admin quota warnings from cached SQLite quota snapshots. Missing usage history snapshots (`raw`, `five_min`, `hourly`, `daily`), retention cleanup, passive quota-window recovery, active quota refresher for dirty/limit-reached accounts, and startup background scheduling.
|
||||
- [ ] **Model catalog:** Rust has a static default catalog, aliases, suffix parsing, read routes, debug summary, backend Codex model fetch/probe, per-plan snapshots, model-plan index feeding account scheduling, `/admin/refresh-models`, and runtime cache persistence. Missing richer metadata fields and optional external model-cache loading if retained.
|
||||
- [ ] **Account scheduling and retry:** Rust has in-memory slots, stale cleanup, strategies, tier priority, preferred/exclude IDs, SQLite startup restore, request-staggering metadata, 429/402/403 fallback across alternate accounts, Cloudflare cooldown skip, successful WebSocket rate-limit header application, non-history WebSocket fallback, previous-response history affinity, and status/token/logout mutation sync for the runtime pool. Missing strict request-interval sleep enforcement, durable quota/cache restore, Cloudflare path-block tracker, cooldown clearing, and richer capacity/health endpoints.
|
||||
|
||||
+32
-2
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
app::state::AppState,
|
||||
http::middleware::RequestId,
|
||||
service::log::{ClearLogs, LogServiceError, LogState, LogStateUpdate},
|
||||
service::log::{ClearLogs, LogListFilter, LogServiceError, LogState, LogStateUpdate},
|
||||
utils::pagination::clamp_limit,
|
||||
};
|
||||
|
||||
@@ -20,6 +20,14 @@ use super::{require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope,
|
||||
pub struct LogsQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub kind: Option<String>,
|
||||
pub level: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
pub route: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status_code: Option<i64>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -55,7 +63,8 @@ pub async fn logs(
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
match state.services.logs.list(query.cursor, limit).await {
|
||||
let cursor = query.cursor.clone();
|
||||
match state.services.logs.list(cursor, limit, query.into()).await {
|
||||
Ok(page) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
@@ -169,6 +178,27 @@ impl From<UpdateLogStateRequest> for LogStateUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogsQuery> for LogListFilter {
|
||||
fn from(query: LogsQuery) -> Self {
|
||||
Self {
|
||||
kind: non_empty(query.kind),
|
||||
level: non_empty(query.level),
|
||||
request_id: non_empty(query.request_id),
|
||||
account_id: non_empty(query.account_id),
|
||||
route: non_empty(query.route),
|
||||
model: non_empty(query.model),
|
||||
status_code: query.status_code,
|
||||
search: non_empty(query.search),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn log_service_error(error: LogServiceError, request_id: String) -> AdminError {
|
||||
match error {
|
||||
LogServiceError::RepositoryUnavailable => AdminError::new(
|
||||
|
||||
+94
-19
@@ -1,4 +1,4 @@
|
||||
use sqlx::{Row, SqlitePool};
|
||||
use sqlx::{QueryBuilder, Row, Sqlite, SqlitePool};
|
||||
|
||||
use crate::{
|
||||
logs::event::{EventLevel, EventLog},
|
||||
@@ -10,6 +10,18 @@ pub struct EventLogRepository {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct EventLogFilters {
|
||||
pub kind: Option<String>,
|
||||
pub level: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
pub route: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status_code: Option<i64>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl EventLogRepository {
|
||||
pub fn new(pool: SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
@@ -41,26 +53,65 @@ impl EventLogRepository {
|
||||
&self,
|
||||
cursor: Option<String>,
|
||||
limit: u32,
|
||||
) -> Result<Page<EventLog>, sqlx::Error> {
|
||||
self.list_filtered(EventLogFilters::default(), cursor, limit)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_filtered(
|
||||
&self,
|
||||
filters: EventLogFilters,
|
||||
cursor: Option<String>,
|
||||
limit: u32,
|
||||
) -> Result<Page<EventLog>, sqlx::Error> {
|
||||
let limit = clamp_limit(limit);
|
||||
let mut rows = if let Some(cursor) = cursor.and_then(|c| decode_cursor(&c)) {
|
||||
sqlx::query(
|
||||
"select id, request_id, kind, level, account_id, route, model, status_code, latency_ms, message, metadata_json, created_at from event_logs where (created_at < ? or (created_at = ? and id < ?)) order by created_at desc, id desc limit ?",
|
||||
)
|
||||
.bind(&cursor.0)
|
||||
.bind(&cursor.0)
|
||||
.bind(&cursor.1)
|
||||
.bind(i64::from(limit + 1))
|
||||
.fetch_all(&self.pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query(
|
||||
"select id, request_id, kind, level, account_id, route, model, status_code, latency_ms, message, metadata_json, created_at from event_logs order by created_at desc, id desc limit ?",
|
||||
)
|
||||
.bind(i64::from(limit + 1))
|
||||
.fetch_all(&self.pool)
|
||||
.await?
|
||||
};
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(
|
||||
"select id, request_id, kind, level, account_id, route, model, status_code, latency_ms, message, metadata_json, created_at from event_logs",
|
||||
);
|
||||
let mut has_condition = false;
|
||||
append_optional_text_filter(&mut builder, &mut has_condition, "kind", filters.kind);
|
||||
append_optional_text_filter(&mut builder, &mut has_condition, "level", filters.level);
|
||||
append_optional_text_filter(
|
||||
&mut builder,
|
||||
&mut has_condition,
|
||||
"request_id",
|
||||
filters.request_id,
|
||||
);
|
||||
append_optional_text_filter(
|
||||
&mut builder,
|
||||
&mut has_condition,
|
||||
"account_id",
|
||||
filters.account_id,
|
||||
);
|
||||
append_optional_text_filter(&mut builder, &mut has_condition, "route", filters.route);
|
||||
append_optional_text_filter(&mut builder, &mut has_condition, "model", filters.model);
|
||||
if let Some(status_code) = filters.status_code {
|
||||
append_condition_prefix(&mut builder, &mut has_condition);
|
||||
builder.push("status_code = ");
|
||||
builder.push_bind(status_code);
|
||||
}
|
||||
if let Some(search) = filters.search {
|
||||
append_condition_prefix(&mut builder, &mut has_condition);
|
||||
let pattern = format!("%{search}%");
|
||||
builder.push("(message like ");
|
||||
builder.push_bind(pattern.clone());
|
||||
builder.push(" or metadata_json like ");
|
||||
builder.push_bind(pattern);
|
||||
builder.push(")");
|
||||
}
|
||||
if let Some(cursor) = cursor.and_then(|c| decode_cursor(&c)) {
|
||||
append_condition_prefix(&mut builder, &mut has_condition);
|
||||
builder.push("(created_at < ");
|
||||
builder.push_bind(cursor.0.clone());
|
||||
builder.push(" or (created_at = ");
|
||||
builder.push_bind(cursor.0);
|
||||
builder.push(" and id < ");
|
||||
builder.push_bind(cursor.1);
|
||||
builder.push("))");
|
||||
}
|
||||
builder.push(" order by created_at desc, id desc limit ");
|
||||
builder.push_bind(i64::from(limit + 1));
|
||||
let mut rows = builder.build().fetch_all(&self.pool).await?;
|
||||
|
||||
let has_next = rows.len() > limit as usize;
|
||||
if has_next {
|
||||
@@ -102,6 +153,30 @@ impl EventLogRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fn append_optional_text_filter(
|
||||
builder: &mut QueryBuilder<Sqlite>,
|
||||
has_condition: &mut bool,
|
||||
column: &'static str,
|
||||
value: Option<String>,
|
||||
) {
|
||||
let Some(value) = value else {
|
||||
return;
|
||||
};
|
||||
append_condition_prefix(builder, has_condition);
|
||||
builder.push(column);
|
||||
builder.push(" = ");
|
||||
builder.push_bind(value);
|
||||
}
|
||||
|
||||
fn append_condition_prefix(builder: &mut QueryBuilder<Sqlite>, has_condition: &mut bool) {
|
||||
if *has_condition {
|
||||
builder.push(" and ");
|
||||
} else {
|
||||
builder.push(" where ");
|
||||
*has_condition = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_event_log(row: sqlx::sqlite::SqliteRow) -> EventLog {
|
||||
EventLog {
|
||||
id: row.get("id"),
|
||||
|
||||
+33
-2
@@ -4,7 +4,10 @@ use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
config::LoggingConfig,
|
||||
logs::{event::EventLog, repository::EventLogRepository},
|
||||
logs::{
|
||||
event::EventLog,
|
||||
repository::{EventLogFilters, EventLogRepository},
|
||||
},
|
||||
utils::pagination::Page,
|
||||
};
|
||||
|
||||
@@ -44,6 +47,18 @@ pub struct LogStateUpdate {
|
||||
pub capture_body: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LogListFilter {
|
||||
pub kind: Option<String>,
|
||||
pub level: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
pub route: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status_code: Option<i64>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl LogService {
|
||||
pub fn new(config: LoggingConfig, repository: Option<EventLogRepository>) -> Self {
|
||||
Self {
|
||||
@@ -56,9 +71,10 @@ impl LogService {
|
||||
&self,
|
||||
cursor: Option<String>,
|
||||
limit: u32,
|
||||
filter: LogListFilter,
|
||||
) -> Result<Page<EventLog>, LogServiceError> {
|
||||
self.repository()?
|
||||
.list(cursor, limit)
|
||||
.list_filtered(EventLogFilters::from(filter), cursor, limit)
|
||||
.await
|
||||
.map_err(|_| LogServiceError::List)
|
||||
}
|
||||
@@ -120,3 +136,18 @@ impl LogService {
|
||||
.ok_or(LogServiceError::RepositoryUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogListFilter> for EventLogFilters {
|
||||
fn from(filter: LogListFilter) -> Self {
|
||||
Self {
|
||||
kind: filter.kind,
|
||||
level: filter.level,
|
||||
request_id: filter.request_id,
|
||||
account_id: filter.account_id,
|
||||
route: filter.route,
|
||||
model: filter.model,
|
||||
status_code: filter.status_code,
|
||||
search: filter.search,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,56 @@ async fn admin_logs_are_cursor_paginated_and_include_request_id() {
|
||||
assert!(body["page"]["nextCursor"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_logs_should_filter_by_kind_level_and_search_text() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let db = dir.path().join("admin-logs-filter.sqlite");
|
||||
let url = format!("sqlite://{}", db.display());
|
||||
let pool = connect_sqlite(&url).await.unwrap();
|
||||
seed_admin_session(&pool, "session_1").await;
|
||||
let repo = EventLogRepository::new(pool.clone());
|
||||
let mut matching = EventLog::new("request", EventLevel::Error, "upstream timeout");
|
||||
matching.id = "log_matching".to_string();
|
||||
matching.route = Some("/v1/responses".to_string());
|
||||
repo.insert(matching).await.unwrap();
|
||||
repo.insert(EventLog::new(
|
||||
"request",
|
||||
EventLevel::Info,
|
||||
"upstream timeout",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
repo.insert(EventLog::new(
|
||||
"account",
|
||||
EventLevel::Error,
|
||||
"upstream timeout",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let app = build_router(AppState::with_pool(test_config(url), pool));
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("GET")
|
||||
.uri("/admin/logs?kind=request&level=error&search=timeout")
|
||||
.header("cookie", "cpr_admin_session=session_1")
|
||||
.header("x-request-id", "req_logs_filter")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response_json(response).await;
|
||||
assert_eq!(body["requestId"], "req_logs_filter");
|
||||
let items = body["data"].as_array().unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0]["id"], "log_matching");
|
||||
assert_eq!(items[0]["route"], "/v1/responses");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_logs_reject_missing_admin_session_cookie() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user