Commit Graph
16 Commits
Author SHA1 Message Date
opsx 7dfbc0b680 feat(redis): 大 key 分页 + 流式导出(含票据鉴权)+ 查询行数上限默认 5000
=== 一、问题:大 key 会把内存打爆,且查询结果没有导出 ===
RedisGetKeyDetail 原来对每种类型都是全量读取:
  string → GET        list → LRANGE 0 -1     set → SMEMBERS
  zset   → ZRANGE 0 -1                        hash → HGETALL
然后整包序列化返回。像 PERP:market:* 这类盘口/kline key,
一个 key 几十万条就会:Go 侧整值入内存 → JSON 序列化翻倍 → 浏览器渲染卡死。

=== 二、后端:键详情分页(统一令牌,兼容四种类型)===
GET /redis/:id/keys/:key?db=&count=&token=
  count  默认 200,硬上限 2000
  token  语义按类型区分,前端无感知:
           list / zset  → 十进制 offset(LRANGE / ZRANGE 区间)
           hash / set   → Redis SCAN 游标(HSCAN / SSCAN)
           string       → 忽略
  返回   total / count / nextToken / truncated / previewCap

string 特殊处理:不能分页,改为预览上限 256KB
  - 用 STRLEN 拿真实大小(O(1))
  - 超过上限时用 GETRANGE 只取前 256KB,truncated=true
  - 完整内容只能靠导出

新增 wrapper 方法:HScan / ZScan / HLen / ZCard / StrLen / GetRange
(HGETALL、SMEMBERS、ZRANGE 0 -1 一律不再用于详情读取)

=== 三、后端:流式导出(两段式票据,避免令牌进 URL 日志)===
① POST /redis/:id/keys/:key/export-ticket   (走 Bearer 鉴权)
     → HMAC-SHA256(JWTSecret, instance|db|key|exp) 生成票据,2 分钟过期,
       绑定具体 instance/db/key,不能跨 key 复用
② GET  /redis/:id/keys/:key/export?db=&format=csv|jsonl&ticket=
     → 注册在鉴权组【之外】,靠票据校验
     为什么这么做:浏览器原生下载(<a download>/window.open)带不了
     Authorization 头;若前端用 axios 取 blob,整份导出会进浏览器内存,
     与「大 key 不爆内存」的目标相悖。

导出实现全程流式(服务端内存 = 单批 + 64KB 缓冲):
  string → GETRANGE 256KB 分块
  list   → LRANGE 每批 1000
  zset   → ZRANGE WITHSCORES 每批 1000
  hash   → HSCAN 游标每批 1000
  set    → SSCAN 游标每批 1000
单次上限 200 万行;CSV 带 UTF-8 BOM(Excel 直接打开不乱码);
列顺:hash(field,value) / list(index,value) / set(member) / zset(rank,member,score)

=== 四、前端:详情面板加分页条 + 4 个导出按钮 ===
RedisKeyDetailPanel:
  - 「共 N 条 · 第 X 页 · 每页 [100/200/500/1000/2000]」+ 上一页/下一页
  - SCAN 游标不可逆,上一页用令牌栈回放
  - string 超预览上限时显示黄色提示「仅预览前 256KB,共 N 字节」
  - 导出四档:当前页 CSV / 当前页 JSON(纯前端,瞬时)
             导出全部 CSV / 导出全部 JSONL(服务端流式,边读边写)
  顺带修掉头注释里「已知限制:大值不分页」的过时说明。

=== 五、查询行数上限:默认 1000 → 5000,并修掉硬编码 ===
用户要求:「默认设置里面有查询最大上限吧 比如 mysql pg 查询最大Limit限制 5000」

发现两个问题:
1. seed 默认值只有 1000(main.go query_limit_max)
2. ExecuteQuery 里还有一段硬编码:
     if input.Limit <= 0 { input.Limit = maxLimit; if input.Limit > 1000 { input.Limit = 1000 } }
   → 管理员把设置改成 5000 也白搭,前端不传 limit 时永远被压回 1000

修复:
  - seed 默认值 1000 → 5000(Value + DefaultValue)
  - getSettingInt("query_limit_max", 1000) → 5000
  - 删掉硬编码 1000 封顶,唯一口径就是设置值
  - 线上已有记录的 value 已通过 PUT /settings 改成 5000(seed 只在缺失时插入,不会自动更新旧值)

对 MySQL / PostgreSQL / Redis / MongoDB 统一生效(ExecuteQuery 按 db_type 分发,
limit 在入口处统一 clamp)。

=== 六、实测结果(线上)===
分页:
  zset PERP:market:kline:symbol_kline_12h_AAVEUSDT_PERP_MARK
        total=83,count=5 → 第1页 5 条 nextToken=5 → 第2页 5 条 nextToken=10,数据不同 ✅
  hash ST:market:ticker:symbol_ticker
        total=27,count=3 → 第1页 3 字段 nextToken=24 → 第2页 3 字段(完全不同)✅
  string PERP:market:depth:symbol_depth_BTCUSDT_PERP
        STRLEN=14192 → 导出 14192 字节,完全一致 ✅(实时 key 两次采样有差异属正常)
导出:
  zset CSV  200 / 30970 字节 / 84 行(header+83)✅
  zset JSONL 200 / 84 行 ✅
  string CSV 200 / 分块 GETRANGE 拼接完整 ✅
  无票据 → 403 ✅   伪造票据 → 403 ✅
查询上限:
  limit=99999 → 5000 行 ✅    limit=0(默认)→ 5000 行 ✅
SQL 导出 CSV(原已存在 ExportQueryCSV,支持 mysql/pg/redis/mongo)200 / 101 行 ✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:68fc996681c6
2026-10-27 09:14:02 +00:00
opsx 36724bba94 fix(queries): MySQL 实例误调 Redis 接口 500 + 点表生成 SELECT + 光标多行闪烁
=== 一、/queries 打开 MySQL 实例报 500 ===
现象:GET /api/v1/redis/5/dbs → 500
     后端日志 redis: invalid reply: "J\x00\x00\x00\n"
     ("J\x00\x00\x00" 是 MySQL 握手包,说明用 Redis 协议连了 3306)

根因:SchemaSidebar 是 mysql/redis 共用的组件,其中
        const redisDbs = useRedisDbs(Number(instanceId));
      是无条件调用(React hooks 不能条件调用),而 useRedisDbs 内部
      只看 instanceId 就请求 /redis/{id}/dbs →
      MySQL 实例也发 Redis 请求 → 后端用 Redis 协议连 MySQL → 500。

修复(前后端双保险):
  1. useRedisDbs 增加 enabled 参数(默认 true),false 时不发请求并复位 [0]/false
  2. SchemaSidebar 传 isRedis:useRedisDbs(Number(instanceId), isRedis)
  3. redis.go getRedisClient 前置校验 DBType != "redis" 直接返回
  4. resp 包新增 UserFacingError / UserFacing(),InternalErr 识别后返回
     400 + 可读原因(不再把所有错误笼统包装成 500 "服务器内部错误")

验证:
  GET /redis/5/dbs(MySQL)→ 400
    {"code":"bad_request","detail":"实例「pre-mysql-只读」的类型是 mysql,
     不是 redis。Redis 接口只适用于 redis 实例;MySQL/PostgreSQL 请使用
     /api/v1/instances/5/databases 等对应接口"}
  GET /instances/5/databases → 200
  GET /redis/4/dbs(Redis 集群)→ 200 {"cluster":true,"dbs":[0]}

=== 二、左侧栏点表不生成 SELECT ===
原交互:单击=展开/折叠,双击=执行 SELECT,Alt+点击=插入表名。
需求:单击表 → 在右侧编辑器生成 SELECT * FROM <db>.<table>

修复:
  1. SchemaSidebar 表节点:箭头图标包 data-chevron 单独触发展开/折叠;
     点表名区域 → onInsert(previewSelectSql(...) + ';\n')
     Alt+点击仍为只插表名;双击仍为直接执行
  2. QueriesPage onInsert:生成的文本以 SELECT 开头时「替换」编辑器内容
     (原来一律追加,会和上一条语句拼成非法 SQL);表名/字段名仍追加

=== 三、SQL 编辑器光标同时在多行闪烁 ===
根因:.cm-cursor 强制了 height:1.25em !important + marginTop:0.15em,
     而 .cm-line 行高是 1.55em,光标高度溢出到相邻行,视觉上像两行同时闪。
修复:光标样式只保留颜色与 1.5px 宽度,height/marginTop 交回 CodeMirror 自行计算。

镜像:lghost/opsxsql:latest + v0.1
2026-10-27 00:58:22 +00:00
opsx f19a84f37e fix(redis): 集群拓扑解析错误,跨机集群节点被强制指向种子机
【现象】Redis 集群实例查 keys 报 500:
  Redis cluster connection failed: dial tcp 172.19.16.16:6005: connect: connection refused

【根因】handler/redis.go 解析 CLUSTER SLOTS:
  nodeAddr := slot.Nodes[0].Addr                    // Redis 上报的真实地址 172.19.16.30:6005
  if idx := strings.LastIndex(nodeAddr, ":"); idx >= 0 {
      nodeAddr = instance.Host + nodeAddr[idx:]     // 强制改写成 种子机主机 + 原端口
  }
  该逻辑假设「集群所有节点在同一台机器、只是端口不同」。
  对跨机集群(16/30/59 三台)会把所有节点地址破坏成
  172.19.16.16:6002 / :6003 / :6005 —— 这些端口在 16 上根本不存在 → refused。

【修复】
  仅当 Redis 上报空 IP / 127.0.0.1 / 0.0.0.0 时才用配置 host 兜底(NAT/端口映射场景),
  其余情况一律保留 Redis 上报的真实主机地址。

【附带数据修复(非代码)】
  实例 5 host: pre-mysql.astx.club → 172.19.16.2
  原因:pre-mysql.astx.club 公网 DNS 解析到 43.135.53.180(另一环境 MySQL),
       认证失败 → 前端数据库下拉为空、无法选择数据库。

【验证】
  GET /api/v1/redis/4/keys?db=0&pattern=*  → 200
    {"cluster":true,"keys":[{"key":"24H_rate_limit_10001_...","type":"string","ttl":42901000},
                            {"key":"CEX:FOLLOW:FOLLOWERS:TRADFI:97127836","type":"hash"},...]}
  GET /api/v1/redis/4/dbs                 → 200 {"cluster":true,"dbs":[0]}
  GET /api/v1/instances/5/databases       → 200 返回 25 个库
    ["cex_activity","cex_admin","cex_agent",...,"cex_user","cex_xxl_job"]

镜像 sha256:165992ef79d5
2026-10-26 18:01:44 +00:00
Logan.Li 6604fdf48e feat(backend): redis key scan service - cursor pagination + cluster + CLI fix
Modeled on opsxdb's redis scan design:
- opaque base64 scan cursor {pattern,db,type,nodes,index,cursor}; server
  validates search conditions match, rejects stale cursors (400)
- soft page cap 200 keys (whole scan batches returned, no key loss),
  max 20 SCAN calls per page; next_cursor until all shards exhausted
- pattern rule: no glob chars -> *kw* contains match; non-glob input
  also probed with TYPE for exact hit (returned as exact, slot-routed
  so cluster/NAT safe)
- pipeline batch TYPE/TTL replaces per-key serial N+1; type filter now
  effective server-side
- cluster: per-master direct SCAN merged across shards (MasterAddrs on
  wrapper), DB0-only, MOVED errors hint at switching deploy mode;
  SlowLog/ClientList aggregate across masters (were cluster-broken)
- GET /redis/:id/dbs (0-15, cluster -> [0])
- POST /redis/:id/keys/:key/field: hash row-level HSET/HDEL (no whole-
  key rewrite)
- RedisExecuteCommand fixed (was always-403): accepts {db, command}
  full-string, quote-aware tokenizer, whitelist on first word, expanded
  read-only set
- RedisUpdateKey preserves previous TTL unless explicitly set/cleared
  (was silently dropping TTL); string type skips the DEL

Verified live against local redis: 200/page no overlap across pages,
cursor 400 on condition change, exact hit, type filter, hash field
edit round-trip, CLI DBSIZE/HGET ok, SET rejected
2026-10-26 18:27:11 +08:00
Logan.Li 16f1f7d65f feat(backend): unified response helpers, refresh token, security hardening
- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
2026-10-26 17:21:00 +08:00
hermes-$(hostname) 0fcdbc5f6c update 2026-07-08 08:53:48 +00:00
Logan.Li 617c15dbfe feat: harden query read-only workflow 2026-07-07 17:20:44 +08:00
Logan.Li 58bbc394e9 feat: enhance redis and production IA 2026-07-07 10:39:33 +08:00
Logan 644981eef4 feat: improve datasource query audit workflows 2026-07-06 23:01:04 +08:00
logan 8af868b3ca fix: 数据源编辑密码保留 + Redis ACL 认证支持
后端:
- UpdateInstance: 空密码和'******'都不更新密码字段,防止编辑后丢失密码
- testRedis/getRedisClient/getRedisMetrics: 增加 Username 参数支持 Redis 6.0+ ACL 认证
- username 为 'default' 或空时走传统密码认证模式

前端:
- EditInstanceDialog: Redis 用户名字段提示'6.0+ ACL 可选',密码 placeholder 改为'不修改请留空'
- CreateInstanceForm: Redis 类型也显示用户名输入框(可选),创建/测试连接时 Redis username 为空则不传
- Redis 提示文案从'无需用户名'改为'6.0+ ACL 支持用户名+密码认证'
2026-05-31 23:55:18 +08:00
logan 9c5cb04cfa fix: Redis集群查询+metrics修复 — 单节点连接替代ClusterClient
- crypto.go: decryptPassword 自动处理 enc: 前缀,避免调用方遗漏
- redis.go: getRedisClient 统一使用 redis.NewClient 单节点连接
  ClusterClient 拓扑发现(CLUSTER SLOTS)在Docker内网IP不可达时超时
  redisClientWrapper 全部方法改为使用 Client (不再走 ClusterClient)
  ScanAllKeys 简化为单节点 SCAN
- connection_tester.go: getRedisMetrics 统一单节点连接采集metrics
  删除 isCluster 分支的 ClusterClient 代码,避免5秒超时
2026-05-31 01:30:10 +08:00
logan 4e6f9eb8fd fix: Redis集群SCAN遍历所有master节点 + 查询结果布局调整\n\n1. ScanAllKeys(): 集群模式用ForEachMaster遍历所有master节点SCAN合并去重\n2. RedisScanKeys/KEYS命令均改用ScanAllKeys,确保集群不遗漏key\n3. QueriesPage布局: sidebar独立左侧,查询结果只在编辑器下方不被sidebar占据宽度 2026-05-30 15:28:38 +08:00
logan 3af65f2350 feat: Redis cluster support + version auto-detect + metrics API 2026-05-30 01:44:58 +08:00
logan 7f09ef8e1a fix: Redis侧边栏扫描也自动补通配符
RedisScanKeys 端点的 pattern 参数无通配符时自动追加 *,
与查询页 KEYS 命令行为一致,输入 match 即匹配 match*
2026-05-28 16:13:50 +08:00
logan f2e93a09c0 feat: 细节打磨 - 全面修复核心功能
后端修复:
- ticket: 添加独立 CancelTicket/RollbackTicket/GetTicketTimeline/GetTicketApprovals handler
  (之前 cancel/rollback 都错误调用 ApproveTicket, timeline/approvals 调用 GetTicket)
- instance_detail: 7个 TODO placeholder 端点全部实现真实 MySQL 查询
  (databases/tables/db-schema/table-detail/processlist/status/users)
- instance_detail: ExecuteQuery 添加真实 Redis 命令执行(只读命令白名单)
- redis: 全部 handler 从 placeholder 改为真实 go-redis 实现
  (info/dbsize/keys/key-detail/slowlog/clients/command/create-key/delete-key)
- router: 添加 sql-assistant/format, sql-assistant/analyze, tickets/audit 路由
- router: 添加 dictionary/*, mongo/* stub 路由(前端不报 404)
- stub_handlers: 新增 SQL 审计/格式化/分析 handler + dictionary/mongo stub
- model: TicketStatistics.Completed JSON tag 改为 success 匹配前端
- ticket: ExecuteTicket 完成 status 改为 success 而非 completed

前端兼容:
- 所有前端 API 路径后端都有对应路由(无 404)
- TicketStatistics 返回 success 字段与前端 stats?.success 对齐
2026-05-26 20:40:45 +08:00
logan 88e5f4e7ee feat: Go后端LDAP真实对接 — AES-256-GCM密码加解密 + 登录LDAP fallback
- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容
- auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户)
- ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表
- 所有 LDAP 操作解密 admin_password 后使用
- 新增/更新密码存储时加密
- router.go: 补充 delete-user 路由
- 添加 go-ldap/v3 依赖
2026-05-25 23:32:15 +08:00