diff --git a/internal/control/request_log_operation_test.go b/internal/control/request_log_operation_test.go new file mode 100644 index 00000000..d1d99fc5 --- /dev/null +++ b/internal/control/request_log_operation_test.go @@ -0,0 +1,25 @@ +package control + +import ( + "testing" + + "gpt-load/internal/execution" +) + +func TestRequestLogQueryFiltersLogicalOperation(t *testing.T) { + t.Parallel() + for _, operation := range []execution.Operation{execution.OperationChatCompletion, execution.OperationWebSearch, execution.OperationResponsesRetrieve} { + query, apiErr := parseRequestLogQuery("operation=" + string(operation)) + if apiErr != nil || query.Operation != operation { + t.Fatalf("operation %q parsed as %#v, error = %v", operation, query.Operation, apiErr) + } + } + for _, query := range []string{"operation=unknown", "operation=", "operation=web_search&operation=chat_completion"} { + if _, apiErr := parseRequestLogQuery(query); apiErr == nil { + t.Fatalf("query %q accepted", query) + } + } + if requestLogQueryUsesInternalFields("operation=web_search") { + t.Fatal("logical operation must remain available in self-scoped logs") + } +} diff --git a/internal/control/request_logs.go b/internal/control/request_logs.go index 257e4c65..2c1f6cb5 100644 --- a/internal/control/request_logs.go +++ b/internal/control/request_logs.go @@ -349,7 +349,7 @@ func parseRequestLogQuery(rawQuery string) (requestlog.ListQuery, *app_errors.AP allowed := map[string]struct{}{ "from_ms": {}, "to_ms": {}, "group_id": {}, "channel_id": {}, "credential_id": {}, "client_model": {}, "upstream_model": {}, "model_consistency": {}, "access_key_id": {}, - "status": {}, "request_id": {}, "protocol": {}, "stream": {}, "final_status_code": {}, + "status": {}, "request_id": {}, "protocol": {}, "operation": {}, "stream": {}, "final_status_code": {}, "usage_state": {}, "cost_state": {}, "pricing_completeness": {}, "cache_present": {}, "attempt_status_code": {}, "failure_category": {}, "error_code": {}, "retry_state": {}, "retry_count_min": {}, "retry_count_max": {}, @@ -443,6 +443,13 @@ func parseRequestLogQuery(rawQuery string) (requestlog.ListQuery, *app_errors.AP } query.Protocol = parsed } + if value, ok := singleQueryValue(values, "operation"); ok { + operation := execution.Operation(value) + if !operation.Valid() { + return requestlog.ListQuery{}, app_errors.ErrValidation + } + query.Operation = operation + } if value, ok := singleQueryValue(values, "stream"); ok { parsed, valid := parseRequestLogBool(value) if !valid { diff --git a/internal/requestlog/operation_query_test.go b/internal/requestlog/operation_query_test.go new file mode 100644 index 00000000..5efec8d3 --- /dev/null +++ b/internal/requestlog/operation_query_test.go @@ -0,0 +1,31 @@ +package requestlog + +import ( + "context" + "testing" + "time" + + "gpt-load/internal/execution" +) + +func TestServiceListFiltersLogicalOperationBeforePagination(t *testing.T) { + db := openRequestLogQueryDB(t) + completed := time.Now() + for index, operation := range []execution.Operation{execution.OperationWebSearch, execution.OperationChatCompletion, execution.OperationWebSearch} { + id := []string{"00000000-0000-4000-8000-000000000701", "00000000-0000-4000-8000-000000000702", "00000000-0000-4000-8000-000000000703"}[index] + row := requestLogQueryRow(id, completed, 1, "model", nil) + row.Operation = string(operation) + createRequestLogQueryRow(t, db, row) + } + service := newRequestLogTestService(db) + query := ListQuery{Operation: execution.OperationWebSearch, Limit: 1} + page, err := service.List(context.Background(), query) + if err != nil || len(page.Items) != 1 || page.Items[0].RequestID != "00000000-0000-4000-8000-000000000703" || page.NextCursor == nil { + t.Fatalf("first page = %#v, error = %v", page, err) + } + query.Cursor = page.NextCursor + page, err = service.List(context.Background(), query) + if err != nil || len(page.Items) != 1 || page.Items[0].RequestID != "00000000-0000-4000-8000-000000000701" || page.NextCursor != nil { + t.Fatalf("second page = %#v, error = %v", page, err) + } +} diff --git a/internal/requestlog/query.go b/internal/requestlog/query.go index 9a147b98..03884ed5 100644 --- a/internal/requestlog/query.go +++ b/internal/requestlog/query.go @@ -55,6 +55,9 @@ func (service *Service) List(ctx context.Context, input ListQuery) (Page, error) if input.Protocol != "" { query = query.Where("protocol = ?", input.Protocol) } + if input.Operation != "" { + query = query.Where("operation = ?", input.Operation) + } if input.Stream != nil { query = query.Where("stream = ?", *input.Stream) } diff --git a/internal/requestlog/types.go b/internal/requestlog/types.go index 7fe4dd85..567e16d5 100644 --- a/internal/requestlog/types.go +++ b/internal/requestlog/types.go @@ -82,6 +82,7 @@ type ListQuery struct { Status telemetry.RequestStatus RequestID string Protocol protocol.Protocol + Operation execution.Operation Stream *bool FinalStatusCode *int UsageState usage.State diff --git a/internal/storage/database_integration_test.go b/internal/storage/database_integration_test.go index fdea2ecc..7507591a 100644 --- a/internal/storage/database_integration_test.go +++ b/internal/storage/database_integration_test.go @@ -123,7 +123,7 @@ func TestExternalDatabaseLifecycle(t *testing.T) { if err := db.Table("schema_migrations").Order("id").Pluck("id", &migrationIDs).Error; err != nil { t.Fatalf("read migration ledger: %v", err) } - if len(migrationIDs) != 16 || migrationIDs[0] != "0001_initial" || + if len(migrationIDs) != 17 || migrationIDs[0] != "0001_initial" || migrationIDs[1] != "0002_access_key_cost_limits" || migrationIDs[2] != "0003_remove_observation_fresh_until" || migrationIDs[3] != "0004_usage_stats_group_activity_index" || @@ -131,8 +131,8 @@ func TestExternalDatabaseLifecycle(t *testing.T) { migrationIDs[5] != "0006_error_decision" || migrationIDs[6] != "0007_access_key_lifecycle" || migrationIDs[7] != "0008_remove_inject_usage_options" || - migrationIDs[8] != "0009_price_multipliers" || migrationIDs[9] != "0010_model_cooldown" || migrationIDs[10] != "0011_custom_access_keys" || migrationIDs[11] != "0012_access_key_mask_prefix" || migrationIDs[12] != "0013_validation_protocol" || migrationIDs[13] != "0014_affinity_kind" || migrationIDs[14] != "0015_group_usage_index" || migrationIDs[15] != "0016_credential_quota_history" { - t.Fatalf("migration ledger = %v, want complete 0001-0016 chain", migrationIDs) + migrationIDs[8] != "0009_price_multipliers" || migrationIDs[9] != "0010_model_cooldown" || migrationIDs[10] != "0011_custom_access_keys" || migrationIDs[11] != "0012_access_key_mask_prefix" || migrationIDs[12] != "0013_validation_protocol" || migrationIDs[13] != "0014_affinity_kind" || migrationIDs[14] != "0015_group_usage_index" || migrationIDs[15] != "0016_credential_quota_history" || migrationIDs[16] != "0017_request_log_operation_index" { + t.Fatalf("migration ledger = %v, want complete 0001-0017 chain", migrationIDs) } if !db.Migrator().HasIndex("usage_stats", "idx_usage_stats_group_bucket") { t.Fatal("usage_stats group activity index is missing") diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go index 47dd11cc..f041a6df 100644 --- a/internal/storage/db_test.go +++ b/internal/storage/db_test.go @@ -673,6 +673,7 @@ func TestAutoMigrateCreatesUsageJournalAndMigrationLedger(t *testing.T) { "0014_affinity_kind", "0015_group_usage_index", "0016_credential_quota_history", + "0017_request_log_operation_index", } if !reflect.DeepEqual(migrationIDs, wantMigrationIDs) { t.Fatalf("schema_migrations IDs = %v, want %v", migrationIDs, wantMigrationIDs) diff --git a/internal/storage/migration.go b/internal/storage/migration.go index 9f10573a..3098058b 100644 --- a/internal/storage/migration.go +++ b/internal/storage/migration.go @@ -107,6 +107,7 @@ var migrations = []migration{ {ID: migrationfiles.ID0014, Up: migrationfiles.Up0014, Validate: migrationfiles.Validate0014, ValidateRecoverable: migrationfiles.ValidateRecoverable0014}, {ID: migrationfiles.ID0015, Up: migrationfiles.Up0015, Validate: migrationfiles.Validate0015, ValidateRecoverable: migrationfiles.ValidateRecoverable0015}, {ID: migrationfiles.ID0016, Up: migrationfiles.Up0016, Validate: migrationfiles.Validate0016, ValidateRecoverable: migrationfiles.ValidateRecoverable0016}, + {ID: migrationfiles.ID0017, Up: migrationfiles.Up0017, Validate: migrationfiles.Validate0017, ValidateRecoverable: migrationfiles.ValidateRecoverable0017}, } func applyMigrations(db *gorm.DB) error { diff --git a/internal/storage/migration_test.go b/internal/storage/migration_test.go index e39b52b7..0dbbfe01 100644 --- a/internal/storage/migration_test.go +++ b/internal/storage/migration_test.go @@ -28,6 +28,7 @@ func TestMigrationRegistryContainsOrderedMigrations(t *testing.T) { migrationfiles.ID0014, migrationfiles.ID0015, migrationfiles.ID0016, + migrationfiles.ID0017, } if len(migrations) != len(wantIDs) { t.Fatalf("migration registry length = %d, want %d", len(migrations), len(wantIDs)) diff --git a/internal/storage/migrations/0017_request_log_operation_index.go b/internal/storage/migrations/0017_request_log_operation_index.go new file mode 100644 index 00000000..8f9933e7 --- /dev/null +++ b/internal/storage/migrations/0017_request_log_operation_index.go @@ -0,0 +1,120 @@ +package migrations + +import ( + "fmt" + + "gorm.io/gorm" +) + +const ID0017 = "0017_request_log_operation_index" +const requestLogOperationIndex0017 = "idx_request_logs_operation_completed_id" + +type requestLog0017 struct { + Operation string `gorm:"column:operation;index:idx_request_logs_operation_completed_id,priority:1"` + CompletedAtMS int64 `gorm:"column:completed_at_ms;index:idx_request_logs_operation_completed_id,priority:2,sort:desc"` + ID string `gorm:"column:id;index:idx_request_logs_operation_completed_id,priority:3,sort:desc"` +} + +func (requestLog0017) TableName() string { return "request_logs" } + +type requestLogOperationIndexColumn0017 struct { + Name string + Descending bool + IsUnique bool + IsValid bool +} + +// Up0017 为操作筛选增加与游标顺序一致的索引,不改历史日志。 +func Up0017(db *gorm.DB) error { + if err := ValidateRecoverable0017(db); err != nil { + return err + } + if !db.Migrator().HasIndex(&requestLog0017{}, requestLogOperationIndex0017) { + if err := db.Migrator().CreateIndex(&requestLog0017{}, requestLogOperationIndex0017); err != nil { + return fmt.Errorf("create request log operation index: %w", err) + } + } + return Validate0017(db) +} + +func ValidateRecoverable0017(db *gorm.DB) error { + if !db.Migrator().HasTable(&requestLog0017{}) { + return fmt.Errorf("request log operation index: request_logs table is missing") + } + if db.Migrator().HasIndex(&requestLog0017{}, requestLogOperationIndex0017) { + return Validate0017(db) + } + return nil +} + +func Validate0017(db *gorm.DB) error { + columns, err := requestLogOperationIndexColumns0017(db) + if err != nil { + return err + } + want := []struct { + name string + desc bool + }{ + {name: "operation"}, + {name: "completed_at_ms", desc: true}, + {name: "id", desc: true}, + } + if len(columns) != len(want) { + return fmt.Errorf("request log operation index is missing or has an unexpected definition") + } + for position, column := range columns { + if column.Name != want[position].name || column.Descending != want[position].desc || column.IsUnique || !column.IsValid { + return fmt.Errorf("request log operation index has an unexpected definition") + } + } + return nil +} + +func requestLogOperationIndexColumns0017(db *gorm.DB) ([]requestLogOperationIndexColumn0017, error) { + var columns []requestLogOperationIndexColumn0017 + var query string + var arguments []any + switch db.Dialector.Name() { + case "postgres": + // GORM 的 PostgreSQL GetIndexes 未按 indkey 的位置排序,也不返回排序方向。 + query = ` + SELECT attribute.attname AS name, + (definition.indoption[key.position - 1] & 1) <> 0 AS descending, + definition.indisunique AS is_unique, definition.indisvalid AS is_valid + FROM pg_index AS definition + JOIN pg_class AS table_relation ON table_relation.oid = definition.indrelid + JOIN pg_namespace AS namespace ON namespace.oid = table_relation.relnamespace + JOIN pg_class AS index_relation ON index_relation.oid = definition.indexrelid + CROSS JOIN LATERAL unnest(definition.indkey) WITH ORDINALITY AS key(attnum, position) + JOIN pg_attribute AS attribute ON attribute.attrelid = table_relation.oid + AND attribute.attnum = key.attnum + WHERE namespace.nspname = current_schema() AND table_relation.relname = 'request_logs' + AND index_relation.relname = ? + ORDER BY key.position` + arguments = []any{requestLogOperationIndex0017} + case "mysql": + query = ` + SELECT column_name AS name, collation = 'D' AS descending, + non_unique = 0 AS is_unique, 1 AS is_valid + FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'request_logs' AND index_name = ? + ORDER BY seq_in_index` + arguments = []any{requestLogOperationIndex0017} + case "sqlite": + query = ` + SELECT column_info.name AS name, column_info.desc <> 0 AS descending, + index_list."unique" <> 0 AS is_unique, 1 AS is_valid + FROM pragma_index_xinfo(?) AS column_info + JOIN pragma_index_list('request_logs') AS index_list ON index_list.name = ? + WHERE column_info.key = 1 + ORDER BY column_info.seqno` + arguments = []any{requestLogOperationIndex0017, requestLogOperationIndex0017} + default: + return nil, fmt.Errorf("request log operation index: unsupported database driver %q", db.Dialector.Name()) + } + if err := db.Raw(query, arguments...).Scan(&columns).Error; err != nil { + return nil, fmt.Errorf("read request log operation index: %w", err) + } + return columns, nil +} diff --git a/internal/storage/migrations/0017_request_log_operation_index_test.go b/internal/storage/migrations/0017_request_log_operation_index_test.go new file mode 100644 index 00000000..478d241a --- /dev/null +++ b/internal/storage/migrations/0017_request_log_operation_index_test.go @@ -0,0 +1,28 @@ +package migrations_test + +import ( + "testing" + + "gpt-load/internal/storage/migrations" +) + +func TestRequestLogOperationIndexRejectsMissingTableAndIndex(t *testing.T) { + db := openInitialTestDatabase(t) + if err := migrations.ValidateRecoverable0017(db); err == nil { + t.Fatal("missing request_logs table accepted") + } + if err := migrations.Up0001(db); err != nil { + t.Fatal(err) + } + if err := migrations.Validate0017(db); err == nil { + t.Fatal("missing operation index accepted") + } + for range 2 { + if err := migrations.Up0017(db); err != nil { + t.Fatal(err) + } + if err := migrations.Validate0017(db); err != nil { + t.Fatal(err) + } + } +} diff --git a/internal/storage/operation_index_migration_test.go b/internal/storage/operation_index_migration_test.go new file mode 100644 index 00000000..113672f4 --- /dev/null +++ b/internal/storage/operation_index_migration_test.go @@ -0,0 +1,98 @@ +package storage + +import ( + "fmt" + "os" + "testing" + + "gorm.io/gorm" +) + +func TestOperationIndexMigrationContract(t *testing.T) { + testOperationIndexMigration(t, openInternalMigrationTestDatabase) +} + +func TestExternalOperationIndexMigrationContract(t *testing.T) { + dsn := os.Getenv("GPT_LOAD_DATABASE_TEST_DSN") + if dsn == "" { + t.Skip("GPT_LOAD_DATABASE_TEST_DSN is not set") + } + testOperationIndexMigration(t, func(t *testing.T) *gorm.DB { + return openExternalIncrementalMigrationDatabase(t, dsn) + }) +} + +func testOperationIndexMigration(t *testing.T, open func(*testing.T) *gorm.DB) { + t.Helper() + for _, scenario := range []string{"fresh", "existing", "interrupted"} { + t.Run(scenario, func(t *testing.T) { + db := open(t) + if scenario != "fresh" { + if err := applyMigrationRegistry(db, migrations[:len(migrations)-1]); err != nil { + t.Fatal(err) + } + if err := db.Table("request_logs").Create(map[string]any{ + "id": "existing", "completed_at_ms": 1, "access_key_id": 1, + "protocol": "openai-responses", "operation": "responses_retrieve", + "client_model": "test", "upstream_model": "test", "status": "error", + "status_code": 500, "duration_ms": 1, "error_summary": "existing diagnostic", + }).Error; err != nil { + t.Fatal(err) + } + if scenario == "interrupted" { + registry := append([]migration(nil), migrations...) + up := registry[len(registry)-1].Up + registry[len(registry)-1].Up = func(tx *gorm.DB) error { + if err := up(tx); err != nil { + return err + } + return fmt.Errorf("interrupt after operation index DDL") + } + if err := applyMigrationRegistry(db, registry); err == nil { + t.Fatal("expected migration interruption") + } + } + } + for range 2 { + if err := AutoMigrate(db); err != nil { + t.Fatal(err) + } + if !db.Migrator().HasIndex("request_logs", "idx_request_logs_operation_completed_id") { + t.Fatal("operation cursor index is missing") + } + if scenario != "fresh" { + var operation string + if err := db.Table("request_logs").Where("id = ?", "existing").Pluck("operation", &operation).Error; err != nil || operation != "responses_retrieve" { + t.Fatalf("existing operation = %q, error = %v", operation, err) + } + } + } + }) + } + t.Run("unexpected index definition", func(t *testing.T) { + db := open(t) + if err := applyMigrationRegistry(db, migrations[:len(migrations)-1]); err != nil { + t.Fatal(err) + } + if err := db.Exec("CREATE INDEX idx_request_logs_operation_completed_id ON request_logs (status)").Error; err != nil { + t.Fatal(err) + } + if err := AutoMigrate(db); err == nil { + t.Fatal("unexpected operation index definition accepted") + } + }) + t.Run("unexpected index direction", func(t *testing.T) { + db := open(t) + if err := applyMigrationRegistry(db, migrations[:len(migrations)-1]); err != nil { + t.Fatal(err) + } + if err := db.Exec( + "CREATE INDEX idx_request_logs_operation_completed_id ON request_logs (operation, completed_at_ms DESC, id ASC)", + ).Error; err != nil { + t.Fatal(err) + } + if err := AutoMigrate(db); err == nil { + t.Fatal("unexpected operation index direction accepted") + } + }) +} diff --git a/web/src/frontends/modern/api/logs.ts b/web/src/frontends/modern/api/logs.ts index b643d980..6dfed101 100644 --- a/web/src/frontends/modern/api/logs.ts +++ b/web/src/frontends/modern/api/logs.ts @@ -3,6 +3,25 @@ import { ApiError, InvalidResponseError } from '@shared/http/errors' import { boolean, integer, list, oneOf, record, text } from './response' export const logStatuses = ['success', 'error', 'incomplete', 'canceled'] as const +export const logOperations = [ + 'chat_completion', + 'responses_create', + 'responses_retrieve', + 'responses_delete', + 'responses_cancel', + 'responses_input_items', + 'responses_compact', + 'responses_input_tokens', + 'count_tokens', + 'responses_passthrough', + 'web_search', + 'images_generate', + 'images_edit', + 'embeddings_create', + 'rerank', + 'list_models', + 'probe', +] as const export const logFilterNames = [ 'from_ms', 'to_ms', @@ -17,6 +36,7 @@ export const logFilterNames = [ 'status', 'request_id', 'protocol', + 'operation', 'stream', 'final_status_code', 'usage_state', @@ -88,6 +108,7 @@ export interface LogEntry { error_code: string error_summary: string affinity_hit: boolean + affinity_kind: string group_id: number | null channel_id: string | null credential_id: number | null @@ -258,6 +279,7 @@ function entry(value: unknown): LogEntry { error_code: text(row.error_code), error_summary: text(row.error_summary), affinity_hit: boolean(row.affinity_hit), + affinity_kind: text(row.affinity_kind), group_id: optionalNumber(row.group_id), channel_id: optionalText(row.channel_id), credential_id: optionalNumber(row.credential_id), diff --git a/web/src/frontends/modern/features/logs/LogCredentialValue.vue b/web/src/frontends/modern/features/logs/LogCredentialValue.vue new file mode 100644 index 00000000..7690c178 --- /dev/null +++ b/web/src/frontends/modern/features/logs/LogCredentialValue.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/web/src/frontends/modern/features/logs/LogDetailPanel.vue b/web/src/frontends/modern/features/logs/LogDetailPanel.vue index 1cee0a31..cfe2102c 100644 --- a/web/src/frontends/modern/features/logs/LogDetailPanel.vue +++ b/web/src/frontends/modern/features/logs/LogDetailPanel.vue @@ -31,6 +31,7 @@ import { createRedactedLogExport } from './log-redacted-export' import LogPricingReceipt from './LogPricingReceipt.vue' import LogValue from './LogValue.vue' import LogModelWarning from './LogModelWarning.vue' +import LogCredentialValue from './LogCredentialValue.vue' const props = defineProps<{ id: string @@ -302,17 +303,18 @@ function resolveRedactedLog(): Promise { >{{ logDuration(attempt.duration_ms, locale) }}
- {{ - attempt.credential_name || - (attempt.credential_id - ? t( - attempt.credential_deleted - ? 'logs.deleted' - : 'logs.unavailableCredential', - ) - : '—') - }} + { />
+ -import { TriangleAlert } from '@lucide/vue' +import { CircleHelp, TriangleAlert } from '@lucide/vue' import { computed } from 'vue' import { useI18n } from 'vue-i18n' import type { LogEntry } from '@modern/api/logs' @@ -9,29 +9,41 @@ import { logModelMismatch } from './log-display' const props = defineProps<{ row: LogEntry; detail?: boolean }>() const { t } = useI18n() const mismatch = computed(() => logModelMismatch(props.row)) +const unknown = computed(() => props.row.model_consistency === 'unknown') +const title = computed(() => + t(mismatch.value ? 'logs.returnedModelMismatch' : 'logs.returnedModelUnknown'), +) const description = computed(() => - t('logs.returnedModelMismatchHint', { - requested: props.row.upstream_model, - returned: props.row.upstream_reported_model, - }), + mismatch.value + ? t('logs.returnedModelMismatchHint', { + requested: props.row.upstream_model || '—', + returned: props.row.upstream_reported_model || t('logs.modelNotObserved'), + }) + : t('logs.returnedModelUnknownHint', { requested: props.row.upstream_model || '—' }), ) @@ -45,9 +57,15 @@ const description = computed(() => color: var(--modern-danger); } .modern-log-model-warning:focus-visible { - outline: var(--modern-focus-width) solid var(--modern-danger); + outline: var(--modern-focus-width) solid currentColor; outline-offset: var(--modern-focus-offset); } +.modern-log-model-warning.is-unknown { + color: var(--modern-info); +} +.modern-log-model-warning.is-detail.is-unknown { + background: var(--modern-info-soft); +} .modern-log-model-warning.is-detail { width: 100%; align-items: flex-start; diff --git a/web/src/frontends/modern/features/logs/LogTableCell.vue b/web/src/frontends/modern/features/logs/LogTableCell.vue index f88cef47..3c2fad8e 100644 --- a/web/src/frontends/modern/features/logs/LogTableCell.vue +++ b/web/src/frontends/modern/features/logs/LogTableCell.vue @@ -15,7 +15,7 @@ import { AppTooltip, } from '@modern/components/ui' import type { LogColumnId } from './log-columns' -import { logTime } from './log-display' +import { logOutputRate, logTime } from './log-display' import LogValue from './LogValue.vue' import LogModelWarning from './LogModelWarning.vue' import LogFilterLink from './LogFilterLink.vue' @@ -128,6 +128,8 @@ function fieldFilter(field: LogColumnId): LogQuery | undefined { return row.status_code ? { final_status_code: String(row.status_code) } : undefined case 'stream': return { stream: String(row.stream) } + case 'operation': + return row.operation ? { operation: row.operation } : undefined case 'usage_state': return { usage_state: row.usage_state } case 'cost_state': @@ -171,6 +173,8 @@ function fieldFilterValue(field: LogColumnId): string { return t(row.stream ? 'logs.yes' : 'logs.no') case 'error_code': return row.error_code + case 'operation': + return row.operation ? t('logs.values.' + row.operation) : '—' default: return field } @@ -288,6 +292,24 @@ function fieldFilterValue(field: LogColumnId): string { +
+
+ + +
+ +
+ +
+
+