后端与接口
需要管理 Grafana Cloud k6…
当用户提到 k6 云测试/运行、需要列出/创建/编辑/启动/中止负载测试、查运行日志或指标、管理项目限制或计划、本地运行脚本或调用 Grafana Cloud k6 的 `/cloud/v6/`、`/cloud/v5/`、Loki endpoint 时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
需要管理 Grafana Cloud k特定
解决思路
- 默认使用 `gcx` CLI;优先运行 `gcx k6 ...` 子命令,用 `gcx help-tree k6` 和 `gcx help-tree k6 <subcommand>` 发现可用子命令;没有子命令时才用 `gcx api`。
- gcx 存在时由它负责 Grafana 侧鉴权,不要在 `gcx api`/`gcx k6` 调用中手动设置 auth header;唯一手动设置的是 Loki 日志查询、浏览器截图/文件获取、Tempo trace 查询的 `X-K6TestRun-Id`。
- 有 gcx 时执行 `gcx login --context <ctx>` 做一次性 OAuth 浏览器登录;用 `gcx --context <ctx> config check` 确认输出 `✔ Connectivity: online`;后续调用继承该 context 的鉴权状态。
- 如果返回 `Invalid or expired token — run gcx login to refresh`,重新运行 `gcx login --context <ctx>`。
- 无 gcx 时先检查 `command -v gcx`;若缺失,所有 endpoint 仍可用 curl 直连 k6 公共主机。
- 无 gcx curl 模式下,每个调用手动带 `Authorization: Bearer <k6_token>` 和 `X-Stack-ID: <int>` 两个 header。
- 无 gcx curl 模式下替换 host:REST(`/cloud/v6/...`、`/cloud/v5/...`、`/cloud-resources/v1/...`、`/insights/...`)走 `https://api.k6.io`;日志(Loki)和 trace(Tempo)走 `https://cloudlogs.k6.io`。
- 无 gcx curl 模式下去掉 `/api/plugins/k6-app/resources/{cloud,logs,insights}` 前缀;`cloud/cloud/` 双写折叠为单个 `/cloud/`。
- 无 gcx curl 模式下向用户询问 k6 API token 和 stack:整数 stack ID 或 Grafana stack URL;若给 URL,用 `GET /cloud/v6/auth` 和 `X-Stack-Url: $STACK_URL` 解析 `stack_id`,缓存到会话;之后用 `X-Stack-ID`。
- 无 gcx curl 模式下,翻译示例:`gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/123` 变为 `curl https://api.k6.io/cloud/v6/test_runs/123 -H ...`;Loki 变为 `curl https://cloudlogs.k6.io/api/v1/query_range?... -H ...`。
- 无 gcx curl 模式下,endpoint 专用 header 如 `X-K6TestRun-Id` 仍需额外带上;`gcx api` 的 flag 特殊行为不适用,用 curl 的 `-o file`、`--data-binary @file`、`-w '%{http_code}'` 等。
- 无 gcx curl 模式下分页语义同 v6 endpoint;`@nextLink` 返回绝对 `https://api.k6.io/...` URL,直接传给 curl,不需插件代理 reshape。
- 使用 `gcx api` 时,REST endpoint 前缀为 `/api/plugins/k6-app/resources/cloud/<k6-path>`;Loki 前缀为 `/api/plugins/k6-app/resources/logs/<loki-path>`;注意 REST 中 `cloud/cloud/` 双写。
- `gcx api` 响应体写 stdout,没有 `-o <file>` 保存 body;`-o` 选输出格式(`json`、`yaml`、`agents`);保存用 shell 重定向 `> file` 或 `$(...)`。
- `gcx api` 请求体用 `-d <string>`、`-d @file` 或 `-d @-`;没有 `--data-binary`;`-d @file` 保留字节。
- `gcx api` 用 `--json field1,field2,...` 做字段选择;`--json list` 或 `--json '?'` 发现可用字段。
- `gcx api` 常向 stderr 打印 `hint:`;管道给 jq 时加 `2>/dev/null`。
- `gcx api` 不直接暴露响应 headers;需要判断 `Content-Type` 时下载 body 后用 `file <path>` 检查。
- 大响应会 spill 到临时文件,stdout 变成 envelope;用 `-o json` 强制内联,或检测 `spilled_to` 后 `json.load(open(d['spilled_to']))` 再读。
- Grafana 插件代理会把 multipart Content-Type 重写成 `application/json`;遇到需要 multipart 的 endpoint(如 `POST /cloud/v6/projects/{id}/load_tests`)改用直接 curl 到 `api.k6.io`,例如 `curl -F name=... -F script=@...`。
- 发现 endpoint:用 `gcx k6` 子命令覆盖常见读取;其余 mutation/niche 读取从 OpenAPI spec 在请求时发现。
- 每会话拉取 OpenAPI spec 并缓存:`gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/openapi > /tmp/k6-openapi.json`。
- 用 jq 先按 `operationId` + `description` 建索引搜索动作(abort、limits、schedule 等)。
- 对选定 operation,用 `jq '.paths["<path>"]["<method>"]' /tmp/k6-openapi.json` 拉全 schema,遇到 `$ref` 再 `jq '.components.schemas.Foo' /tmp/k6-openapi.json` 解析。
- 按同一 path 构建 `gcx api` 调用并按 §2 加前缀;授权已由 gcx 注入,不要自己加。
- 调用 list endpoint 时默认按 `created` 降序(最新在前)并用 20 分页;具体 query 参数名从 OpenAPI schema 中发现(如 `ordering=-created`、`order_by=created.desc`、`sort=-created`、`limit=20`、`page_size=20`)。
- 需要全量枚举 v6 list 时,注意每页上限 1000 行并返回 `@nextLink`;不要依赖 `gcx k6 runs list --limit 0`,它不 follow `@nextLink` 且默认升序,会静默返回最旧 1000 行。
- 全量枚举用 `gcx api` 对 v6 endpoint 设置 `$orderby=created desc` 取最新切片,或循环 `@nextLink` 直到缺失;OData 参数有 `$orderby`(值 `<field> <direction>`,如 `created desc`)、`$top`(默认/上限 1000)、`$skip`、`$count`。
- 分页循环默认 `$orderby=created desc` 和 `-o json`;`@nextLink` 是绝对 URL,先 `urllib.parse.urlparse` 再拼回插件代理前缀;只需最近数据时停在第一页。
- Metrics 在 v5 API(`/cloud/v5/...`),使用 Prometheus 风格 OData URL function-call(`query_range_k6(query='...',metric='...')`);构造前阅读 `references/metrics.md`。
- 查日志:Loki 插件前缀 `/api/plugins/k6-app/resources/logs/`;只手动带 `X-K6TestRun-Id`,gcx 处理授权。
- 查日志先取 run 对象,用 `.created` 作为 `START`,`.ended` 作为 `END`;若 `ended` 为 null 用当前时间;jq `fromdateiso8601` 前去掉亚秒 `.123Z`。
- 构建 LogQL 时必须包含 `{test_run_id="<run_id>"}` stream selector;可叠加 `| level=~"(error|warn)"` 或 `|= "specific text"`。
- LogQL 查询 URL-encode 后请求 `api/v1/query_range`,参数含 `query`、`direction`、`start`、`end`、`limit=1000`;将原始响应保存到 `/tmp/run_${RUN_ID}_logs.json`,不要内联大日志。
- 用 jq 从日志文件生成紧凑摘要:`status`、streams 数、total_entries、stream_labels;需要具体条目再从文件 `jq '.data.result[].values[]'` 取,不要重复查询。
- 日志方向与窗口:`direction=forward` + `start = run.created` 找第一批错误;`direction=backward` + `end = run.ended`(或 now)找最近输出;Loki 保留期通常长于 k6 cascade-delete,已删除子 run 的日志可能仍可查。
- 编辑脚本前区分两个 GET:`/cloud/v6/load_tests/<test_id>/script` 返回当前可变脚本并支持 PUT;`/cloud/v6/test_runs/<run_id>/script` 返回该 run 的只读快照;两者会漂移。
- 用 run-vs-run 脚本 diff 调查历史时,GET 两个 run 的 `/test_runs/<id>/script` 并在本地 diff;快照 endpoint 只能 GET,不能改历史字节。
- 脚本 GET 可能返回单 js/ts 文件或 k6 tar 归档(普通 tar 或 gzip);先下载后用 `file /tmp/script_body` 判断类型,因为 `gcx api` 不暴露 response headers。
- 编辑 k6 tar 归档时:`tar -xf /tmp/script_body -C /tmp/extracted/` 解出,改 JS,再用 `k6 archive /tmp/edited_script.js -O /tmp/new_archive.tar` 重建;不要手动 `tar` 重打包,否则 `metadata.json` 陈旧导致运行时错误。
- 安全编辑单文件脚本:1 拉当前脚本到 `/tmp/script_current.js`;2 备份为 `/tmp/script_backup_$(date +%s).js`;3 编辑到 `/tmp/script_new.js`。
- 编辑后做 `k6 inspect /tmp/script_new.js | head -20` 解析检查。
- 做 1 迭代 smoke:在文件内修改(`sed 's/iterations: [0-9][0-9]*/iterations: 1/' /tmp/script_new.js > /tmp/script_1iter.js`),运行 `k6 run --quiet /tmp/script_1iter.js`;`--iterations` CLI flag 会破坏 browser scenarios。
- PUT 脚本:`gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script -X PUT -H 'Content-Type: application/octet-stream' -d @/tmp/script_new.js`;body 必须是 raw script/archive 字节。
- PUT 后重新 GET 并比较 sha256;两个 hash 必须匹配;不要依赖 `updated` 时间戳,它不会因脚本 PUT 而更新。
- 若脚本 PUT 返回 415,检查 `-H 'Content-Type: application/octet-stream'` 是否传入;用 `gcx api -vvv` 看请求/响应 trace。
- 获取浏览器截图:走 `cloud-resources/v1/files/` 两步流程;两个调用都带 `X-K6TestRun-Id`,否则 HTTP 422。
- 截图第 1 步:`gcx api /api/plugins/k6-app/resources/cloud/cloud-resources/v1/files/index -H 'X-K6TestRun-Id: $RUN_ID' -o json 2>/dev/null > /tmp/files_${RUN_ID}.json`,得到截图路径数组;路径中 `screenshots/screenshots/` 双写是真实的,原样传给第 2 步。
- 截图第 2 步:构造 JSON body `{service:"aws_s3", operation:"download", files:[{name:...}]}`,POST `.../cloud-resources/v1/files/generate-pre-signed-url`,带 `X-K6TestRun-Id` 和 `Content-Type: application/json`,保存输出。
- 截图第 3 步:对每个 pre-signed URL 直接 `curl -sS -o /tmp/run_${RUN_ID}_files/$(basename "$NAME") "$URL"`,无需 gcx/auth;URL 有效期 24h(`X-Amz-Expires=86400`),过期则重跑第 2 步。
- 截图注意:先查 `files/index` 再签名,`generate-pre-signed-url` 不校验文件存在;一次批量请求所有文件;只有调用 `page.screenshot()` 的 browser run 才有条目,纯协议 run 返回 `[]`。
- 获取浏览器 trace:Browser run 产生 OTel spans;Tempo 插件前缀与 Loki 相同;每次调用必须带 `X-K6TestRun-Id`,否则 search/fetch 返回 HTTP 401 `Test run ID missing`。
- Trace 流程两步:TraceQL search 找 iteration root spans,再按 trace ID fetch 完整 trace;完整 trace 很大(约 100 KB pretty-printed,约 50 KB compact),要 join+summarise。
- Trace 第 1 步:取 run 的 `created`/`ended`,去掉亚秒,`START=$((START - 60))`、`END=$((END + 60))`;从 run 对象 `.options.scenarios` 的 keys 选 scenario 名。
- Trace 第 2 步:构造 TraceQL `{ name = "iteration" && span.test.iteration.number >= 0 && span.test.vu >= 0 && span.test.scenario = "SCENARIO" }`,URL-encode;请求 `/api/plugins/k6-app/resources/logs/api/v1/tempo/api/search?start=${START}&end=${END}&q=${QE}&limit=500`,带 `X-K6TestRun-Id`,`-o json` 保存到 `/tmp/traces_${RUN_ID}.json`;`start`/`end` 必须是 unix 秒,RFC3339 会 HTTP 400。
- Trace 第 3 步:从 search 结果取 `jq -r '.traces[0].traceID'`,请求 `/api/plugins/k6-app/resources/logs/api/v1/tempo/api/traces/$TRACE_ID`,带 `X-K6TestRun-Id`,`-o json` 保存到 `/tmp/trace_${TRACE_ID}.json`;响应为 OTLP `{ batches: [ { resource, scopeSpans: [ { spans: [...] } ] } ] }`。
- Trace 紧凑摘要:用 jq 管道把 OTLP 折叠成约 1–2 KB 文本,包含 span-name rollup(count/total ms/max ms)、slowest spans(含 `navigation.url`/`page.goto.url`)、web-vital ratings、span errors count;`attrval` 用 `has("stringValue"/"intValue"/"boolValue"/"doubleValue")` 判断,不要用 `//` 链,因为 `false` 会被当 missing。
- Trace 注意:search/fetch 必须带 header;`start`/`end` 用 unix 秒;`q` 是 TraceQL 不是 LogQL,predicate 写成 `span.<attribute>` 并用 `&&` 组合;scenario 名来自 run 的 `.options.scenarios` keys。
- Trace 注意:search 结果里的 `traceID` 是 hex,`/traces/<id>` 接受;OTLP batch 内部 `traceId`/`spanId`/`parentSpanId` 是 base64;父子关联直接用 base64 的 `parentSpanId == spanId`。
- Trace 有用属性键:`navigation.url`、`page.goto.url`、`screenshot.path`、`web_vital.name`、`web_vital.value`、`web_vital.rating`、`test.scenario`、`test.vu`、`test.iteration.number`、`k6.test_run_id`;OTel value envelope 是 `{stringValue|intValue|boolValue|doubleValue}`。
- Cloud Insights:查询 `/resources/insights/` 插件路由;gcx 处理授权,无需额外 header;它是 run 完成后的启发式分析。
- Insights 第 1 步:`BASE="/api/plugins/k6-app/resources/insights/insights/api/v1/testrun/$RUN_ID"`;GET `$BASE/executions`,响应是 `{ "executions": [...] }` 不是裸数组;取最后一个 `jq -r '.executions[-1].id'` 作为 `EXEC_ID`。
- Insights 第 2 步:GET `$BASE/executions/$EXEC_ID/audits` 保存到 `/tmp/insights_audits_${RUN_ID}.json`,得到审计定义(id/title/description/weight)。
- Insights 第 3 步:GET `$BASE/executions/$EXEC_ID/audits/results` 保存到 `/tmp/insights_results_${RUN_ID}.json`,得到结果(audit_id/status/score/explanation/actions)。
- Insights 两个响应顶层键都是 `audits`,但内容不同;用 `result.audit_id == audit.id` 交叉 join。
- Insights 紧凑摘要:用 jq 以 `--slurpfile` 读两个文件,`INDEX(.id)` 建定义索引,输出每个 audit 的 title、description、status、status_reason、score(null→n/a,binary→pass/fail,numeric→值)、explanation 前 300 字符、actions;15 个 audit 约 5 KB。
- Insights 注意:`status: "succeeded"` 只表示审计执行了,结论看 `score`;`status: "failed"` 表示审计没跑(如 `missing data`),应视为无信号而非失败;numeric 阈值因审计而异,以 `explanation` 为准。
- Insights 注意:`actions` 是最可操作部分,有非空 actions 的审计优先展示;`.executions[]` 按时间顺序,选最后一个;run 未完成或数据不足时 executions 可能为空,`length == 0` 时优雅退出。
- 本地 k6 CLI:用 `k6 inspect script.js | head -20` 做解析检查,用 `k6 run --quiet script.js` 本地运行。
- 本地 `k6 cloud run` 鉴权:`TOKEN=$(gcx --context <ctx> k6 auth token)`,`STACK=$(gcx --context <ctx> config view --minify -o json | jq -r '.contexts[].grafana.server')`;然后 `k6 cloud login --token "$TOKEN" --stack "$STACK"`,再 `k6 cloud run script.js`。
- k6 cloud 配置 `~/.config/k6/cloud.json` 是单 context;切换 gcx context 后要重新 `k6 cloud login`,否则仍指向旧 stack。
- k6 退出码:`0` 通过,`99` threshold 失败,其他为脚本/运行时错误。
- Gotcha:401 `Invalid or expired token — run gcx login to refresh` → 重跑 `gcx login --context <ctx>`。
- Gotcha:路径看起来对但 403/404 → 检查 REST endpoint 是否漏了双写 `cloud/cloud/`。
- Gotcha:脚本 PUT 415 → 加 `-H 'Content-Type: application/octet-stream'`;用 `-vvv` 或 `--log-http-payload` 检查请求。
- Gotcha:脚本 PUT 返回 200 但不生效 → 不依赖 `updated` 时间戳,用 GET 后 sha256 校验。
- Gotcha:run 状态 `passed` 但 check 失败 → 零观测阈值会被报为 pass,单独 `check()` 不会让 run 失败;加 `'checks{check:<name>}': ['rate==1.0']`,catch 中用 `check(null, {"script completed":()=>false})` 强制观测。
- Gotcha:Loki 最近 run 查询无结果 → 检查 `X-K6TestRun-Id` header 是否带。
- Gotcha:`gcx k6 runs list --limit 0` 返回最旧 1000 行 → 用 `gcx api` + `$orderby=created desc` 或 `@nextLink` 循环。
- Gotcha:`gcx k6 load-tests update -f` 返回 `✔ Updated` 但不生效 → 字段不在 v6 PATCH schema 会被静默丢弃(`PatchLoadTestApiModel` 仅 `name`、`baseline_test_run_id`,`additionalProperties: false`);项目移动用 `/move`,其他 mutation 查 OpenAPI schema,并 re-GET 确认。
- Gotcha:`gcx k6 load-tests list --project-id <id>` 不过滤 → 用 `GET /cloud/v6/projects/{id}/load_tests` 或 jq 客户端过滤 `select(.project_id == X)`。
- Gotcha:通过 `gcx api` 发 multipart POST 得到 HTTP 415 `Unsupported media type application/json` → 插件代理改写 multipart;改用直连 curl `api.k6.io`。
- Gotcha:curl 模式直连 `api.k6.io`/`cloudlogs.k6.io` 返回 401 → 检查 `Authorization: Bearer …` 和 `X-Stack-ID`;用 `/cloud/v6/auth` 从 stack URL 解析。
- Gotcha:curl 模式 `404` 调 `api.k6.io/api/plugins/...` → 去掉 `/api/plugins/k6-app/resources/{cloud,logs,insights}` 前缀。
- Mutations:`gcx k6 <resource> update -f` 遵循 v6 PATCH schema;schema 外字段被静默丢弃但仍打印 `✔ Updated`;很多 mutation 有专用 endpoint。
- 专用 mutation endpoint:移动测试 `PUT /cloud/v6/load_tests/{id}/move` body `{"project_id": <int>}`;启动 run `POST /cloud/v6/load_tests/{id}/start` body `{}` 或 run options。
- 启动 run 后轮询 `GET /cloud/v6/test_runs/{id}` 直到 `status` 为 `completed` 或 `aborted`;同时检查 `result`,`status: completed` + `result: error` 表示配置/基础设施失败;非 `passed` 立即查日志。
- 专用 mutation endpoint:中止运行 `POST /cloud/v6/test_runs/{id}/abort` 空 body;设置/覆盖 schedule `POST /cloud/v6/load_tests/{id}/schedule` body recurrence_rule 或 cron;停用/启用 schedule `POST /cloud/v6/schedules/{id}/{deactivate,activate}` 空 body。
- 专用 mutation endpoint:创建 load test 用 multipart `POST /cloud/v6/projects/{id}/load_tests`,form parts `name` + `script`;持久化 run `POST /cloud/v6/test_runs/{id}/save` 空 body(配对 `/unsave`)。
- 可通过 `update -f` 的 PATCH 字段:load test 仅 `name`、`baseline_test_run_id`;project 仅 `name`;两个 schema 都 `additionalProperties: false`,其他字段会被过滤。
- 在假设字段可变前,读取 OpenAPI 中该 operation 的 request schema 交叉确认。
- 移动测试示例:`gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/<test_id>/move -X PUT -H 'Content-Type: application/json' -d '{"project_id": <new_project_id>}'`;成功可能是 HTTP 204 空 body;之后 re-GET `gcx --context <ctx> k6 load-tests get <test_id> -o json | jq '{id, name, project_id}'` 验证。
- 移动测试会连 runs 和 run history 一起移动;schedule 也跟随(按 `load_test_id` 关联)。
- 级联行为:删除 load test 会级联删除 schedule;删除有 running test 的 project 会 HTTP 409 `Cannot delete project with a running test.`;删除项目前用 `GET /cloud/v6/projects/{id}/load_tests` 盘点内容。
- 验证规则:每次 mutation 先记下预期变更(字段、数量、状态),re-GET 确认;若未生效,检查是否应使用专用 endpoint 而非 `update -f`。
适用边界 · 注意事项
- 不适用于非 Grafana Cloud k6 的本地或自托管 k6 场景:没有 k6 API token、Grafana Cloud stack 或 gcx context 时无法使用这些云 API。
- 无法获取 k6 API token,或不知道 stack ID/URL,或没有对应 Grafana Cloud stack 权限时,不应尝试这些管理操作。
- 只想做本地 k6 脚本解析和运行且不涉及云上测试、日志、指标、计划、项目时,只需本地 k6 CLI,不必使用本经验中的 gcx/curl API 流程。