k6-manage
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGrafana Cloud k6 — interaction reference
Grafana Cloud k6 — 交互参考
The default path is the CLI. When gcx isn't installed, every
endpoint here is still reachable via direct curl against k6's public
hosts — see §1.2 for the auth-header and host-translation rules. Two
principles shape the rest:
gcx- gcx owns Grafana-side auth (when present). It injects the right
headers on every call, so you should not set auth headers yourself.
The only header you ever set by hand is , on Loki log queries (§4), browser screenshot/file fetches (§6), and Tempo trace queries (§7) — anything else gets overwritten or causes conflicts. In curl mode the auth headers are manual; see §1.2.
X-K6TestRun-Id - Reach for subcommands first. They wrap the common paths with friendlier ergonomics and handle pagination. Discover what's available with
gcx k6 ...(and drill in further withgcx help-tree k6); fall back togcx help-tree k6 <subcommand>only when no subcommand exists for what you need.gcx api
默认方式是使用 CLI。如果未安装gcx,此处的所有端点仍可通过直接curl调用k6的公共主机访问——请参阅§1.2了解身份验证标头和主机转换规则。其余内容遵循两个原则:
gcx- **当gcx存在时,由gcx处理Grafana端的身份验证。**它会在每次调用中注入正确的标头,因此您无需自行设置身份验证标头。您唯一需要手动设置的标头是,用于Loki日志查询(§4)、浏览器截图/文件获取(§6)和Tempo追踪查询(§7)——其他任何标头都会被覆盖或导致冲突。在curl模式下,身份验证标头需要手动设置;请参阅§1.2。
X-K6TestRun-Id - **优先使用子命令。**它们以更友好的交互方式封装了常用操作路径,并处理分页。使用
gcx k6 ...(并通过gcx help-tree k6进一步深入)了解可用的子命令;仅当所需操作没有对应的子命令时,才退而使用gcx help-tree k6 <subcommand>。gcx api
1. Authentication
1. 身份验证
1.1 With gcx (default)
1.1 使用gcx(默认方式)
bash
gcx login --context <ctx> # one-time OAuth, browser flow
gcx --context <ctx> config check # expect "✔ Connectivity: online"Once a context is logged in, every and call
inherits its auth state. If a call returns "Invalid or expired token —
run gcx login to refresh", the OAuth session has lapsed — re-run
.
gcx api ...gcx k6 ...gcx login --context <ctx>bash
gcx login --context <ctx> # one-time OAuth, browser flow
gcx --context <ctx> config check # expect "✔ Connectivity: online"登录上下文后,所有和调用都会继承其身份验证状态。如果调用返回*"Invalid or expired token — run gcx login to refresh"*,说明OAuth会话已过期——重新运行即可。
gcx api ...gcx k6 ...gcx login --context <ctx>1.2 Without gcx — direct curl
1.2 不使用gcx——直接调用curl
Check first. If it's missing, every endpoint in this
skill is still reachable directly against k6's public hosts — three
things change versus the gcx examples elsewhere:
command -v gcx- Auth headers are manual. Set both on every call:
Authorization: Bearer <k6_token>X-Stack-ID: <int>
- Hosts replace the plugin proxy.
- REST (,
/cloud/v6/...,/cloud/v5/...,/cloud-resources/v1/...) →/insights/...https://api.k6.io - Logs (Loki) and traces (Tempo) →
https://cloudlogs.k6.io
- REST (
- No prefix. Drop it; everything after that prefix in the gcx examples is the real k6 path. The doubled
/api/plugins/k6-app/resources/{cloud,logs,insights}quirk from §2 collapses to a singlecloud/cloud/— the first one was just the proxy route./cloud/
首先检查。如果未安装gcx,本技能中的所有端点仍可直接调用k6的公共主机访问——与gcx示例相比,有三点变化:
command -v gcx- **身份验证标头需手动设置。**每次调用都要设置以下两个标头:
Authorization: Bearer <k6_token>X-Stack-ID: <int>
- 主机替换插件代理。
- REST(、
/cloud/v6/...、/cloud/v5/...、/cloud-resources/v1/...)→/insights/...https://api.k6.io - 日志(Loki)和追踪(Tempo)→
https://cloudlogs.k6.io
- REST(
- **无需前缀。**移除该前缀;gcx示例中该前缀后的内容即为k6的真实路径。§2中出现的
/api/plugins/k6-app/resources/{cloud,logs,insights}重复问题会简化为单个cloud/cloud/——第一个/cloud/只是代理路由。cloud
Obtaining the credentials
获取凭据
Don't guess these — prompt the user once per session for:
-
k6 API token — long-lived bearer; the same valuewould print when gcx is configured.
gcx k6 auth token -
Stack — either the integer stack ID (used directly in) or a Grafana stack URL (e.g.
X-Stack-ID). If the user supplies a URL, resolve it to an ID once withhttps://myorg.grafana.net, which takes the URL in theGET /cloud/v6/authheader and returnsX-Stack-Url:{stack_id, default_project_id}bashSTACK_ID=$(curl -sS https://api.k6.io/cloud/v6/auth \ -H "Authorization: Bearer $K6_TOKEN" \ -H "X-Stack-Url: $STACK_URL" \ | jq -r '.stack_id')Cache the resolved ID for the session — every subsequent call needs it in. (Note:X-Stack-IDis the only endpoint that takes/cloud/v6/authinstead ofX-Stack-Url— it's how you cross the gap from "user-known URL" to "API-required ID".)X-Stack-ID
请勿猜测这些值——每个会话提示用户一次,获取以下信息:
-
k6 API令牌——长期有效的Bearer令牌;与gcx配置完成后输出的值相同。
gcx k6 auth token -
堆栈——可以是整数类型的堆栈ID(直接用于)或Grafana堆栈URL(例如
X-Stack-ID)。如果用户提供URL,通过https://myorg.grafana.net将其解析为ID一次,该接口接受GET /cloud/v6/auth标头中的URL,并返回X-Stack-Url:{stack_id, default_project_id}bashSTACK_ID=$(curl -sS https://api.k6.io/cloud/v6/auth \ -H "Authorization: Bearer $K6_TOKEN" \ -H "X-Stack-Url: $STACK_URL" \ | jq -r '.stack_id')将会话中解析得到的ID缓存起来——后续每次调用都需要在中使用该ID。(注意:X-Stack-ID是唯一接受/cloud/v6/auth而非X-Stack-Url的端点——这是从“用户已知URL”转换为“API所需ID”的关键。)X-Stack-ID
Translation cheat-sheet
转换对照表
| gcx form (plugin proxy) | curl form (direct) |
|---|---|
| |
| |
| |
| |
| |
| |
In every slot above, send both auth headers:
.
-H ...-H "Authorization: Bearer $K6_TOKEN" -H "X-Stack-ID: $STACK_ID"Notes:
- Endpoint-specific headers gcx leaves to you — on log, trace, and files endpoints (§4, §6, §7) — are still required in addition to the auth pair.
X-K6TestRun-Id - The flag quirks in §2 (spill envelope,
gcx apifiltering,--json fieldfor output format) don't apply to curl. Use plain curl flags:-oto save body,-o filefor PUT payloads,--data-binary @filefor status code, etc.-w '%{http_code}' - Pagination semantics (,
$orderby,$top,$skipfrom §3) are properties of the v6 endpoints themselves and work identically over curl. The@nextLinkURL returned by the server is already an absolute@nextLinkURL — pass it back to curl unchanged; the plugin-proxy reshape in §3 isn't needed.https://api.k6.io/...
| gcx格式(插件代理) | curl格式(直接调用) |
|---|---|
| |
| |
| |
| |
| |
| |
在上述所有位置,都要发送两个身份验证标头:。
-H ...-H "Authorization: Bearer $K6_TOKEN" -H "X-Stack-ID: $STACK_ID"注意事项:
- gcx交由您自行设置的端点特定标头——日志、追踪和文件端点(§4、§6、§7)所需的——仍需额外添加到身份验证标头对中。
X-K6TestRun-Id - §2中提到的标志特性(溢出信封、
gcx api过滤、--json field指定输出格式)不适用于curl。请使用标准curl标志:-o保存响应体,-o file用于PUT负载,--data-binary @file获取状态码等。-w '%{http_code}' - 分页语义(§3中的、
$orderby、$top、$skip)是v6端点本身的特性,通过curl调用时工作方式完全相同。服务器返回的@nextLinkURL已是完整的@nextLinkURL——直接传递给curl即可;无需像§3中那样进行插件代理转换。https://api.k6.io/...
2. How gcx api
paths are shaped
gcx api2. gcx api
路径的构成
gcx apiWhen no subcommand exists, fall back to against the Grafana
plugin-proxy routes:
gcx api- REST API (,
/cloud/v6/) — prefix with/cloud/v5/./api/plugins/k6-app/resources/cloud/<k6-path> - Logs (Loki) — prefix with
.
/api/plugins/k6-app/resources/logs/<loki-path>
| k6 path | gcx invocation |
|---|---|
| |
| |
Loki | |
Note the doubled in every REST path — the first
is the proxy route, the second is k6's namespace.
cloud/cloud/cloud/cloud/v{N}/当没有对应的子命令时,退而使用调用Grafana插件代理路由:
gcx api- REST API(、
/cloud/v6/)——前缀为/cloud/v5/。/api/plugins/k6-app/resources/cloud/<k6-path> - 日志(Loki)——前缀为。
/api/plugins/k6-app/resources/logs/<loki-path>
| k6路径 | gcx调用方式 |
|---|---|
| |
| |
Loki | |
注意每个REST路径中都有重复的——第一个是代理路由,第二个是k6的命名空间。
cloud/cloud/cloud/cloud/v{N}/gcx api
flag quirks
gcx apigcx api
标志特性
gcx apigcx api-
Response body is written to stdout. There is noflag for saving the body;
-o <file>selects output format (-o,json,yaml). Use shell redirection (agents) or> filecapture instead.$(...) -
Request body uses,
-d <string>, or-d @file(stdin). There is no-d @-;--data-binaryalready preserves bytes.-d @file -
Field selection without jq:returns only the listed fields, and
--json field1,field2,...(or--json list) discovers what's available. Often cleaner than piping into jq for shallow extractions.--json '?' -
Stderr noise: gcx prints a one-lineto stderr on most invocations. Pipelines into
hint:should redirect withjqto avoid surprises.2>/dev/null -
Response headers are not directly exposed by. When a branch in the workflow hinges on
gcx api(e.g. script GET in §5), inspect the downloaded body withContent-Typeinstead.file <path> -
Large responses spill to a temp file, with stdout reduced to a wrapper envelope. The threshold is a few tens of KB, so even moderately-sized list endpoints trigger it. The envelope looks like this:json
{"spilled_to":"/var/folders/.../gcx-results-<n>.json", "bytes":2365037, "preview_sample":["@nextLink","value"], "message":"Response too large for stdout..."}Naivepatterns silently return empty in this case — the envelope has nojson.loads(stdout).get('value', [])key. Two reliable fixes:value- Pass to force inline output regardless of size (recommended for scripts that parse the body). The agent-mode default formatting is what triggers the spill;
-o jsonopts out.-o json - Or detect the envelope and re-read from the spilled file:
python
d = json.loads(stdout) if 'spilled_to' in d: d = json.load(open(d['spilled_to']))
Either works;is fewer lines.-o json - Pass
-
The Grafana plugin proxy rewritesto
Content-Type: multipart/....application/jsonitself forwards yourgcx apiheader correctly (visible in-H "Content-Type: ..."traces), but the upstream plugin proxy rewrites multipart Content-Types to JSON before they reach k6's API. The net effect: endpoints that require multipart bodies — notably--log-http-payloadfor test creation, which takesPOST /cloud/v6/projects/{id}/load_tests+nameas form parts — returnscriptno matter what header you set on the gcx side. Fall back to direct curl againstHTTP 415 "Unsupported media type \"application/json\""(§1.2) for these endpoints —api.k6.iobuilds the multipart body for you and bypasses the plugin proxy. Other Content-Type values (e.g.curl -F name=... -F script=@...for the script-update PUT in §5) are forwarded through the proxy unchanged.application/octet-stream
gcx api-
响应体会输出到标准输出。没有用于保存响应体的标志;
-o <file>用于选择输出格式(-o、json、yaml)。请使用shell重定向(agents)或> file捕获输出。$(...) -
请求体使用、
-d <string>或-d @file(标准输入)。没有-d @-;--data-binary已保留字节内容。-d @file -
无需jq即可选择字段:仅返回指定字段,
--json field1,field2,...(或--json list)可查看可用字段。对于浅层提取,这通常比管道输出到jq更简洁。--json '?' -
标准错误输出信息:gcx在大多数调用时会向标准错误输出打印一行提示。管道输出到jq时应使用
hint:重定向,避免意外问题。2>/dev/null -
响应标头无法通过直接查看。当工作流程依赖
gcx api时(例如§5中的脚本GET请求),请使用Content-Type检查下载的响应体。file <path> -
大响应会溢出到临时文件,标准输出仅保留一个包装信封。阈值约为几十KB,因此即使是中等大小的列表端点也会触发此行为。信封格式如下:json
{"spilled_to":"/var/folders/.../gcx-results-<n>.json", "bytes":2365037, "preview_sample":["@nextLink","value"], "message":"Response too large for stdout..."}简单的模式在此情况下会静默返回空值——信封中没有json.loads(stdout).get('value', [])键。有两种可靠的解决方法:value- **传递**强制将输出内联,无论大小如何(推荐用于解析响应体的脚本)。代理模式的默认格式会触发溢出;
-o json可禁用此行为。-o json - 或者检测信封并从溢出文件重新读取:
python
d = json.loads(stdout) if 'spilled_to' in d: d = json.load(open(d['spilled_to']))
两种方法都可行;代码行数更少。-o json - **传递
-
Grafana插件代理会将重写为
Content-Type: multipart/...。application/json本身会正确转发您设置的gcx api标头(在-H "Content-Type: ..."跟踪中可见),但上游插件代理会在请求到达k6 API之前将multipart类型的Content-Type重写为JSON。最终结果:需要multipart请求体的端点——尤其是用于创建测试的--log-http-payload(需要POST /cloud/v6/projects/{id}/load_tests+name作为表单部分)——无论您在gcx端设置什么标头,都会返回script。对于这些端点,请退而使用直接curl调用HTTP 415 "Unsupported media type \"application/json\""(§1.2)——api.k6.io会为您构建multipart请求体并绕过插件代理。其他Content-Type值(例如§5中脚本更新PUT请求的curl -F name=... -F script=@...)会通过代理正常转发。application/octet-stream
3. Discovering and calling endpoints
3. 发现并调用端点
gcx k6gcx help-tree k6gcx k6gcx help-tree k6Workflow
工作流程
-
Fetch the spec once per session (it's large; cache to):
/tmpbashgcx api /api/plugins/k6-app/resources/cloud/cloud/v6/openapi > /tmp/k6-openapi.json -
Index by+
operationIdfirst — this keeps the working payload small while you search:descriptionbashjq '[.paths | to_entries[] | .key as $p | .value | to_entries[] | {path: $p, method: .key, operationId: .value.operationId, description: .value.description}]' /tmp/k6-openapi.jsonGrep this for the action you need ("abort", "limits", "schedule", …). -
Pull the chosen operation's full schema — parameters, request body, response — resolving anyindirections:
$refbashjq '.paths["<path>"]["<method>"]' /tmp/k6-openapi.json # Then for each "$ref": "#/components/schemas/Foo": jq '.components.schemas.Foo' /tmp/k6-openapi.json -
Build thecall against the same path, prefixed per §2. Authorization is already injected by gcx — do not add it yourself.
gcx api
-
每个会话获取一次规范(文件较大;缓存到):
/tmpbashgcx api /api/plugins/k6-app/resources/cloud/cloud/v6/openapi > /tmp/k6-openapi.json -
首先按+
operationId索引——这样在搜索时可保持工作负载较小:descriptionbashjq '[.paths | to_entries[] | .key as $p | .value | to_entries[] | {path: $p, method: .key, operationId: .value.operationId, description: .value.description}]' /tmp/k6-openapi.json对此输出进行grep搜索,查找所需操作(例如“abort”、“limits”、“schedule”等)。 -
提取所选操作的完整模式——参数、请求体、响应——解析所有引用:
$refbashjq '.paths["<path>"]["<method>"]' /tmp/k6-openapi.json # 对于每个"$ref": "#/components/schemas/Foo": jq '.components.schemas.Foo' /tmp/k6-openapi.json -
构建调用,使用§2中提到的前缀路径。gcx已注入身份验证——请勿自行添加。
gcx api
List requests
列表请求
When the operation you're calling is a list endpoint, default to
ordering by descending (newest first) and paginating
by 20 — both via whatever query parameter names the OpenAPI schema
for that operation exposes. Names vary (,
, , , ),
which is exactly why discovering them from the spec in step 3 matters.
Newest-first means the entries the user usually cares about land in
the first page; a page size of 20 keeps the response small enough to
summarise without burning context.
createdordering=-createdorder_by=created.descsort=-createdlimit=20page_size=20当调用的操作是列表端点时,默认按**降序排序**(最新的在前)并按20条分页——两者均通过该操作的OpenAPI模式公开的查询参数实现。参数名称各不相同(、、、、),这正是步骤3中从规范中发现它们的原因。最新在前意味着用户通常关心的条目会出现在第一页;每页20条可保持响应足够小,便于汇总而不会占用过多上下文。
createdordering=-createdorder_by=created.descsort=-createdlimit=20page_size=20Full enumeration: paginating with @nextLink
@nextLink完整枚举:使用@nextLink
分页
@nextLinkWhen you need to enumerate all rows (not just the newest page), the
v6 list endpoints cap each response at 1000 rows (the default
) and return an field pointing at the next page.
does NOT auto-follow and
defaults to ascending order — for tests with >1000 historical runs it
silently returns the oldest 1000, not the newest. The
practical effect: summarised from this output can
be wildly stale (e.g. a daily-scheduled test that's been running for
years will show a from ~3 years ago). Use
against the v6 endpoint with for the
newest-first slice, or loop on until it's absent for full
enumeration.
$top@nextLinkgcx k6 runs list --limit 0@nextLink(first/last_run)last_rungcx api$orderby=created desc@nextLinkThe v6 list endpoints expose OData-style query parameters:
| Param | Purpose |
|---|---|
| Ordering — value must be |
| Page size, default 1000 (also the cap) |
| Offset ( |
| Include total count in response |
Two practical defaults for the loop: so the
first page contains the newest runs (most of the time the agent only
cares about those), and to opt out of the spill envelope
(see §2) so the parsing is uniform across pages.
$orderby=created desc-o jsonpython
import json, subprocess, urllib.parse
CTX = "<stack>"
TEST_ID = "<test_id>"
PROXY = "/api/plugins/k6-app/resources/cloud"
all_runs = []当需要枚举所有行(而不仅仅是最新的一页)时,v6列表端点会将每个响应限制为1000行(默认值),并返回字段指向下一页。不会自动跟随,且默认按升序排序——对于历史运行超过1000次的测试,它会静默返回最早的1000次运行,而非最新的。实际影响:从此输出汇总的可能会严重过时(例如,每日调度的测试运行多年后,会显示约3年前的时间)。请使用调用v6端点并设置获取最新在前的切片,或循环使用直到该字段不存在以完成完整枚举。
$top@nextLinkgcx k6 runs list --limit 0@nextLink(first/last_run)last_rungcx api$orderby=created desc@nextLinkv6列表端点公开OData风格的查询参数:
| 参数 | 用途 |
|---|---|
| 排序——值必须为 |
| 每页大小,默认1000(也是上限) |
| 偏移量( |
| 在响应中包含总行数 |
循环的两个实用默认值:使第一页包含最新的运行(大多数情况下代理只关心这些),以及禁用溢出信封(请参阅§2),使各页面的解析方式一致。
$orderby=created desc-o jsonpython
import json, subprocess, urllib.parse
CTX = "<stack>"
TEST_ID = "<test_id>"
PROXY = "/api/plugins/k6-app/resources/cloud"
all_runs = []Start with newest first; -o json keeps the body inline for clean json.loads().
从最新的开始;-o json保持响应体内联,便于json.loads()解析。
path = (
f"{PROXY}/cloud/v6/load_tests/{TEST_ID}/test_runs"
"?%24orderby=created%20desc&%24top=1000"
)
while path:
r = subprocess.run(["gcx", "--context", CTX, "api", path, "-o", "json"],
capture_output=True, text=True, timeout=60)
d = json.loads(r.stdout)
all_runs.extend(d.get("value", []))
nxt = d.get("@nextLink")
if not nxt:
path = None
continue
# @nextLink comes back as an absolute http://api.k6.io/... URL with
# OData params already encoded ($skip, $top, $orderby). Reshape its
# path+query into the plugin-proxy prefix:
p = urllib.parse.urlparse(nxt)
path = f"{PROXY}{p.path}" + (f"?{p.query}" if p.query else "")
Same pattern works for any v6 list endpoint that returns `@nextLink`,
not just `/test_runs`. If you only need the few most-recent items,
stop the loop after the first page — with `$orderby=created desc` that
page is already the newest 1000.path = (
f"{PROXY}/cloud/v6/load_tests/{TEST_ID}/test_runs"
"?%24orderby=created%20desc&%24top=1000"
)
while path:
r = subprocess.run(["gcx", "--context", CTX, "api", path, "-o", "json"],
capture_output=True, text=True, timeout=60)
d = json.loads(r.stdout)
all_runs.extend(d.get("value", []))
nxt = d.get("@nextLink")
if not nxt:
path = None
continue
# @nextLink返回的是完整的http://api.k6.io/... URL,
# 已编码OData参数($skip、$top、$orderby)。将其
# 路径+查询转换为插件代理前缀:
p = urllib.parse.urlparse(nxt)
path = f"{PROXY}{p.path}" + (f"?{p.query}" if p.query else "")
此模式适用于任何返回`@nextLink`的v6列表端点,而不仅仅是`/test_runs`。如果只需要最近的几个条目,在第一页后停止循环即可——使用`$orderby=created desc`时,该页已是最新的1000次运行。Metrics
指标
Time-series and aggregate metrics live on the v5 API
() — Prometheus-like query semantics inside OData-style
URL function-calls (), not
captured by the v6 OpenAPI. The full endpoint reference, selector
syntax, query methods per metric type, and worked examples are inlined
in — read that before
constructing a metrics query.
/cloud/v5/...query_range_k6(query='...',metric='...')references/metrics.md时间序列和聚合指标位于v5 API()——采用OData风格的URL函数调用()实现类Prometheus的查询语义,未被v6 OpenAPI捕获。完整的端点参考、选择器语法、各指标类型的查询方法和示例都内联在中——构建指标查询前请先阅读该文档。
/cloud/v5/...query_range_k6(query='...',metric='...')references/metrics.mdLogs
日志
See §4.
请参阅§4。
4. Logs (Loki via gcx api
)
gcx api4. 日志(通过gcx api
调用Loki)
gcx apiThe plugin proxies Loki under . The
only header you supply by hand is ; gcx handles
authorization.
/api/plugins/k6-app/resources/logs/X-K6TestRun-Idbash
RUN_ID="<run_id>"插件将Loki代理到下。您唯一需要手动设置的标头是;gcx处理身份验证。
/api/plugins/k6-app/resources/logs/X-K6TestRun-Idbash
RUN_ID="<run_id>"Scope the log window to the run itself: created
for start, ended
createdended将日志窗口范围限定为运行本身:created
为开始时间,ended
createdendedfor end. If ended
is null the run is still in progress — fall back
ended为结束时间。如果ended
为null,说明运行仍在进行中——退而使用"now"。仅当运行对象的
endedto "now". Use a wider hard-coded window only when the run object's
时间戳不符合您的调查需求时,才使用更宽的硬编码窗口。
timestamps don't fit what you're investigating.
—
RUN=$(gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID 2>/dev/null)
RUN=$(gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID 2>/dev/null)
k6 emits sub-second precision (e.g. "...:58.984041Z"); jq's
k6输出的时间精度为亚秒级(例如"...:58.984041Z");jq的
fromdateiso8601 only accepts whole seconds, so strip the fraction.
fromdateiso8601仅接受整秒,因此需要去掉小数部分。
START=$(echo "$RUN" | jq -r '.created | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601')
END=$(echo "$RUN" | jq -r 'if .ended then (.ended | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601) else (now | floor) end')
QUERY=$(printf '{test_run_id="%s"}' "$RUN_ID" | jq -sRr @uri)
START=$(echo "$RUN" | jq -r '.created | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601')
END=$(echo "$RUN" | jq -r 'if .ended then (.ended | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601) else (now | floor) end')
QUERY=$(printf '{test_run_id="%s"}' "$RUN_ID" | jq -sRr @uri)
Save the raw response to /tmp/ — log payloads can be large; streaming
将原始响应保存到/tmp/——日志负载可能很大;流式输出会占用过多上下文。从文件汇总信息,仅在需要时使用jq查询特定条目。
them inline burns context. Summarise from the file, then jq into
—
specific entries only when needed.
—
gcx api "/api/plugins/k6-app/resources/logs/api/v1/query_range?query=${QUERY}&direction=backward&start=${START}&end=${END}&limit=1000"
-H "X-K6TestRun-Id: $RUN_ID" 2>/dev/null > /tmp/run_${RUN_ID}_logs.json
-H "X-K6TestRun-Id: $RUN_ID" 2>/dev/null > /tmp/run_${RUN_ID}_logs.json
gcx api "/api/plugins/k6-app/resources/logs/api/v1/query_range?query=${QUERY}&direction=backward&start=${START}&end=${END}&limit=1000"
-H "X-K6TestRun-Id: $RUN_ID" 2>/dev/null > /tmp/run_${RUN_ID}_logs.json
-H "X-K6TestRun-Id: $RUN_ID" 2>/dev/null > /tmp/run_${RUN_ID}_logs.json
Tight summary — counts and stream labels only:
简洁汇总——仅包含计数和流标签:
jq '{status, streams: (.data.result | length),
total_entries: ([.data.result[].values | length] | add),
stream_labels: [.data.result[].stream]}' /tmp/run_${RUN_ID}_logs.json
Pull individual entries from `/tmp/run_${RUN_ID}_logs.json` as needed
(e.g. `jq '.data.result[].values[]' …`) rather than re-running the
query.
Every LogQL query **must** include the `{test_run_id="<run_id>"}` stream
selector — the plugin proxy partitions logs by run and rejects (or
returns nothing for) queries without it. Layer additional filters on
top of that selector:
- `{test_run_id="<run_id>"}` — everything
- `{test_run_id="<run_id>"} | level=~"(error|warn)"` — errors and warnings only
- `{test_run_id="<run_id>"} |= "specific text"` — substring filter
Direction & window tips:
- **Scope `start`/`end` to the run's `created`/`ended` fields**, not to
wall-clock "last hour" — otherwise queries silently miss anything
older than the window. The only reason to deviate is when you
specifically want a different interval (e.g. surrounding context).
- `direction=forward` with `start = run.created` to find the *first* errors.
- `direction=backward` with `end = run.ended` (or now, if still running)
to find the *most recent* output.
- Loki retention typically outlives k6's cascade-delete, so logs of a
deleted child run may still be queryable for a while.
---jq '{status, streams: (.data.result | length),
total_entries: ([.data.result[].values | length] | add),
stream_labels: [.data.result[].stream]}' /tmp/run_${RUN_ID}_logs.json
根据需要从`/tmp/run_${RUN_ID}_logs.json`中提取单个条目(例如`jq '.data.result[].values[]' …`),而非重新运行查询。
每个LogQL查询**必须**包含`{test_run_id="<run_id>"}`流选择器——插件代理按运行划分日志,没有该选择器的查询会被拒绝(或返回空结果)。可在该选择器基础上添加额外过滤条件:
- `{test_run_id="<run_id>"}`——所有日志
- `{test_run_id="<run_id>"} | level=~"(error|warn)"`——仅错误和警告日志
- `{test_run_id="<run_id>"} |= "specific text"`——子字符串过滤
方向和窗口提示:
- **将`start`/`end`范围限定为运行的`created`/`ended`字段**,而非“最近一小时”的时钟时间——否则查询会静默遗漏窗口之外的任何内容。仅当您需要特定的时间间隔(例如相关上下文)时才偏离此规则。
- 设置`direction=forward`且`start = run.created`以查找*第一个*错误。
- 设置`direction=backward`且`end = run.ended`(如果仍在运行则使用当前时间)以查找*最新的*输出。
- Loki的保留期通常长于k6的级联删除时间,因此已删除子运行的日志可能仍可查询一段时间。
---5. Editing a test script safely
5. 安全编辑测试脚本
Two distinct script endpoints
两个不同的脚本端点
There are two GET endpoints that return a k6 script body, and they are
not interchangeable:
| Endpoint | What it returns | When to use |
|---|---|---|
| The current load-test script (mutable; supports PUT) | Editing the script; reading current state |
| The script snapshot bundled into a specific run (read-only) | Investigating what a past run actually executed; diffing across versions |
The two can drift apart: if the load-test script is edited after a
run completes, that run's bundled snapshot stays frozen at what it
executed, while the load-test endpoint serves the new current version.
The bundled-snapshot endpoint is GET-only — you cannot mutate
historical bytes.
The safe-edit recipe below uses the load-test endpoint (the editable
one). For run-vs-run script diffs (e.g. "did the script change between
the last passing and first failing run?"), GET both runs'
and diff them locally.
/test_runs/<id>/script有两个GET端点返回k6脚本内容,且它们不可互换:
| 端点 | 返回内容 | 使用场景 |
|---|---|---|
| 当前负载测试脚本(可修改;支持PUT) | 编辑脚本;读取当前状态 |
| 特定运行中打包的脚本快照(只读) | 调查过去的运行实际执行了什么;对比不同版本的差异 |
两者可能会出现差异:如果负载测试脚本在运行完成后被编辑,该运行的打包快照会保持执行时的状态不变,而负载测试端点会返回新的当前版本。打包快照端点仅支持GET——您无法修改历史字节内容。
下面的安全编辑流程使用负载测试端点(可编辑的那个)。如需对比不同运行的脚本差异(例如“最后一次通过的运行和第一次失败的运行之间脚本是否有变化?”),请GET两个运行的并在本地对比差异。
/test_runs/<id>/scriptScript body format
脚本内容格式
The script GET endpoint returns one of two shapes — always detect which
before assuming the format:
-
Single js/ts file. The recipe below handles this case directly.
-
k6 tar archive (plain tar or gzipped). A multi-file project bundle (entry script + imported modules + assets). To edit: extract the JS file, modify it, then useto rebuild the archive from the modified JS. Do not manually repack with
k6 archive— the archive contains atarwith parsed options (projectID, thresholds, scenarios) thatmetadata.jsonregenerates correctly from the script source. Manual repacking preserves stale metadata and causes runtime errors (e.g. projectID mismatch).k6 archivebash# Edit a k6 tar archive tar -xf /tmp/script_body -C /tmp/extracted/ # extract # ... edit the JS file ... k6 archive /tmp/edited_script.js -O /tmp/new_archive.tar # rebuild
gcx apifile(1)bash
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
> /tmp/script_body
file /tmp/script_body脚本GET端点返回两种格式之一——在假设格式前务必先检测:
-
单个js/ts文件。下面的流程直接处理此情况。
-
k6 tar归档文件(普通tar或gzip压缩)。多文件项目包(入口脚本 + 导入模块 + 资源)。如需编辑:提取JS文件,修改后使用从修改后的JS重新构建归档文件。请勿手动使用
k6 archive重新打包——归档文件包含tar,其中包含从脚本源正确生成的解析选项(projectID、阈值、场景)。手动打包会保留过时的元数据并导致运行时错误(例如projectID不匹配)。metadata.jsonbash# 编辑k6 tar归档文件 tar -xf /tmp/script_body -C /tmp/extracted/ # 提取 # ... 编辑JS文件 ... k6 archive /tmp/edited_script.js -O /tmp/new_archive.tar # 重新构建
gcx apifile(1)bash
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
> /tmp/script_body
file /tmp/script_bodyExpected output (examples):
预期输出(示例):
ASCII text → js/ts source
ASCII text → js/ts源码
POSIX tar archive → k6 tar bundle
POSIX tar archive → k6 tar包
gzip compressed data → gzipped tar bundle
gzip compressed data → gzip压缩的tar包
The PUT body must be the raw script (or archive) bytes
(`application/octet-stream`). gcx forwards the request to k6 unchanged.
```bash
ID="<load_test_id>"
PUT请求体必须是原始脚本(或归档文件)的字节内容(`application/octet-stream`)。gcx会将请求原样转发给k6。
```bash
ID="<load_test_id>"1. Pull current script (single-file js/ts case)
1. 获取当前脚本(单文件js/ts情况)
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
/tmp/script_current.js
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
/tmp/script_current.js
2. Backup
2. 备份
cp /tmp/script_current.js /tmp/script_backup_$(date +%s).js
cp /tmp/script_current.js /tmp/script_backup_$(date +%s).js
3. Edit → /tmp/script_new.js
3. 编辑 → /tmp/script_new.js
4. Local parse-only sanity check
4. 本地仅解析检查
k6 inspect /tmp/script_new.js | head -20
k6 inspect /tmp/script_new.js | head -20
5. 1-iteration smoke (edit IN-FILE; --iterations CLI flag breaks browser scenarios)
5. 1次迭代的冒烟测试(在文件内编辑;--iterations CLI标志会破坏浏览器场景)
sed 's/iterations: [0-9]+/iterations: 1/' /tmp/script_new.js > /tmp/script_1iter.js
k6 run --quiet /tmp/script_1iter.js # exit 0 = pass, 99 = threshold fail
sed 's/iterations: [0-9]+/iterations: 1/' /tmp/script_new.js > /tmp/script_1iter.js
k6 run --quiet /tmp/script_1iter.js # 退出码0=通过,99=阈值未通过
6. PUT — Content-Type must be application/octet-stream
6. PUT请求——Content-Type必须为application/octet-stream
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"
-X PUT
-H "Content-Type: application/octet-stream"
-d "@/tmp/script_new.js"
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"
-X PUT
-H "Content-Type: application/octet-stream"
-d "@/tmp/script_new.js"
7. Verify by re-fetching and comparing sha256 — the two hashes MUST match.
7. 通过重新获取并对比sha256验证——两个哈希值必须匹配。
Do not rely on the updated
timestamp; it does not bump on script PUT.
updated请勿依赖updated
时间戳;脚本PUT后该时间戳不会更新。
updatedgcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
/tmp/script_verify.js shasum -a 256 /tmp/script_new.js /tmp/script_verify.js
If the PUT returns 415, double-check that `-H "Content-Type:
application/octet-stream"` made it through (some shell quoting mistakes
can drop it). Pass `-vvv` to `gcx api` for the request/response trace
when debugging.
---gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
/tmp/script_verify.js shasum -a 256 /tmp/script_new.js /tmp/script_verify.js
如果PUT请求返回415,请仔细检查`-H "Content-Type: application/octet-stream"`是否已正确设置(某些shell引用错误可能会导致该标头丢失)。调试时请向`gcx api`传递`-vvv`以获取请求/响应跟踪信息。
---6. Browser screenshots
6. 浏览器截图
Browser-module runs write screenshot PNGs to per-run S3 storage —
that's the only artifact type the files API surfaces today. They're
not exposed via ; retrieval goes through a separate
plugin route in a two-step flow. Both
calls require the same header used for log queries
(§4) — gcx still handles auth, but the run ID must be supplied by
hand or the endpoint rejects with HTTP 422.
/cloud/v6/cloud-resources/v1/files/X-K6TestRun-Idbash
RUN_ID="<run_id>"浏览器模块运行会将截图PNG写入每个运行的S3存储——这是文件API目前公开的唯一 artifact 类型。它们不会通过暴露;需要通过单独的插件路由分两步获取。两次调用都需要使用日志查询(§4)中用到的相同标头——gcx仍处理身份验证,但必须手动提供运行ID,否则端点会返回HTTP 422错误。
/cloud/v6/cloud-resources/v1/files/X-K6TestRun-Idbash
RUN_ID="<run_id>"1. Index — JSON array of screenshot paths owned by this run. Empty
1. 索引——该运行拥有的截图路径JSON数组。如果运行未生成截图(例如非浏览器测试,或
array if the run took none (e.g. non-browser tests, or browser
未调用page.screenshot()的浏览器运行),则返回空数组。
runs that didn't call page.screenshot()).
—
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
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/files_${RUN_ID}.json
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
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/files_${RUN_ID}.json
Each entry looks like:
每个条目格式如下:
"7542817/files/screenshots/screenshots/login-success.png"
"7542817/files/screenshots/screenshots/login-success.png"
The doubled "screenshots/screenshots/" segment is real, not a typo —
重复的"screenshots/screenshots/"段是真实存在的,并非输入错误——
pass the path back unchanged to step 2.
第二步中请原样传递该路径。
2. Request pre-signed download URLs for the files you want. The
2. 请求所需文件的预签名下载URL。请求体必须为JSON;Content-Type很重要。
request body must be JSON; Content-Type matters.
—
FILES=$(jq -c '[.[] | {name: .}]' /tmp/files_${RUN_ID}.json)
PAYLOAD=$(jq -nc --argjson files "$FILES"
'{service:"aws_s3", operation:"download", files:$files}')
'{service:"aws_s3", operation:"download", files:$files}')
gcx api /api/plugins/k6-app/resources/cloud/cloud-resources/v1/files/generate-pre-signed-url
-X POST
-H "X-K6TestRun-Id: $RUN_ID"
-H "Content-Type: application/json"
-d "$PAYLOAD" -o json 2>/dev/null > /tmp/presigned_${RUN_ID}.json
-X POST
-H "X-K6TestRun-Id: $RUN_ID"
-H "Content-Type: application/json"
-d "$PAYLOAD" -o json 2>/dev/null > /tmp/presigned_${RUN_ID}.json
FILES=$(jq -c '[.[] | {name: .}]' /tmp/files_${RUN_ID}.json)
PAYLOAD=$(jq -nc --argjson files "$FILES"
'{service:"aws_s3", operation:"download", files:$files}')
'{service:"aws_s3", operation:"download", files:$files}')
gcx api /api/plugins/k6-app/resources/cloud/cloud-resources/v1/files/generate-pre-signed-url
-X POST
-H "X-K6TestRun-Id: $RUN_ID"
-H "Content-Type: application/json"
-d "$PAYLOAD" -o json 2>/dev/null > /tmp/presigned_${RUN_ID}.json
-X POST
-H "X-K6TestRun-Id: $RUN_ID"
-H "Content-Type: application/json"
-d "$PAYLOAD" -o json 2>/dev/null > /tmp/presigned_${RUN_ID}.json
3. Download each pre-signed URL directly — they're plain S3 GETs, no
3. 直接下载每个预签名URL——它们是普通的S3 GET请求,无需gcx和身份验证标头(签名已包含在URL中)。URL的有效期为24小时(X-Amz-Expires=86400);过期后请重新运行第二步。
gcx and no auth header (the signature is in the URL). The URLs
—
are valid for 24h (X-Amz-Expires=86400); re-run step 2 if they
—
expire.
—
mkdir -p /tmp/run_${RUN_ID}files
jq -r '.urls[] | [.name, .pre_signed_url] | @tsv' /tmp/presigned${RUN_ID}.json |
while IFS=$'\t' read -r NAME URL; do
curl -sS -o "/tmp/run_${RUN_ID}_files/$(basename "$NAME")" "$URL"
done
undefinedmkdir -p /tmp/run_${RUN_ID}files
jq -r '.urls[] | [.name, .pre_signed_url] | @tsv' /tmp/presigned${RUN_ID}.json |
while IFS=$'\t' read -r NAME URL; do
curl -sS -o "/tmp/run_${RUN_ID}_files/$(basename "$NAME")" "$URL"
done
undefinedNotes worth knowing
值得注意的事项
- Index before signing. does not validate that the file exists — it will happily mint a URL for any key, and the 404 only surfaces when you try to download (S3 returns
generate-pre-signed-url). Always derive the file list from<Error><Code>NoSuchKey</Code>..., not from a guess at the path shape.files/index - Batch the sign request. is an array, so request all the URLs you need in one POST rather than one call per file — same 24h expiry covers the whole batch.
files - Browser tests vs. protocol tests. Only runs that called
have entries in the index; a pure protocol/HTTP run will return
page.screenshot(). Don't assume the index is non-empty.[]
- 先索引再签名。不会验证文件是否存在——它会为任何密钥生成URL,只有在您尝试下载时才会出现404错误(S3返回
generate-pre-signed-url)。请始终从<Error><Code>NoSuchKey</Code>...获取文件列表,而非猜测路径格式。files/index - 批量签名请求。是数组,因此请在一次POST请求中获取所有需要的URL,而非每个文件调用一次——同一24小时有效期适用于整个批量请求。
files - **浏览器测试与协议测试。**只有调用了的运行才会在索引中有条目;纯协议/HTTP运行会返回
page.screenshot()。请勿假设索引非空。[]
7. Browser traces (Tempo via gcx api
)
gcx api7. 浏览器追踪(通过gcx api
调用Tempo)
gcx apiBrowser-module runs emit OTel spans for every iteration, navigation,
locator click, screenshot, web-vital observation, etc. The plugin
proxies a Tempo backend under
(same prefix as Loki — see §4), and the same header
is required on every call. Without it, both search and fetch return
HTTP 401 .
/api/plugins/k6-app/resources/logs/X-K6TestRun-Id"Test run ID missing"Retrieval is a two-step flow: TraceQL search → fetch full trace by
ID. The full trace is large (≈100 KB on disk for a 120-span iteration
because pretty-prints; ~50 KB compact), so the
join+summarise step is what keeps agent context lean.
-o jsonbash
RUN_ID="<run_id>"浏览器模块运行会为每次迭代、导航、定位器点击、截图、Web性能指标观测等事件发送OTel span。插件将Tempo后端代理到下(与Loki使用相同前缀——请参阅§4),每次调用都需要使用相同的标头。如果没有该标头,搜索和获取都会返回HTTP 401错误。
/api/plugins/k6-app/resources/logs/X-K6TestRun-Id"Test run ID missing"获取流程分为两步:TraceQL搜索 → 通过ID获取完整追踪。完整追踪数据较大(对于包含120个span的迭代,格式化后约为100 KB;压缩后约50 KB),因此合并+汇总步骤可保持代理上下文简洁。
-o jsonbash
RUN_ID="<run_id>"1. Find the run's start/end window and pick the scenario name.
1. 获取运行的开始/结束窗口并选择场景名称。
Same caveat as §4: strip the sub-second fraction before
与§4相同的注意事项:在fromdateiso8601之前去掉亚秒小数部分,如果运行仍在进行中则退而使用"now"。添加一个小缓冲区,因为span的时间可能略晚于运行对象报告的ended
时间。
endedfromdateiso8601, and fall back to "now" if the run is still
—
running. Add a small buffer because spans can land slightly
—
after the run object reports ended
.
ended—
RUN=$(gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID 2>/dev/null)
START=$(echo "$RUN" | jq -r '.created | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601')
END=$( echo "$RUN" | jq -r 'if .ended then (.ended | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601) else (now|floor) end')
START=$((START - 60))
END=$((END + 60))
SCENARIO="ui" # whatever your scenario is named; check run.ui.exec / k6 script
RUN=$(gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID 2>/dev/null)
START=$(echo "$RUN" | jq -r '.created | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601')
END=$( echo "$RUN" | jq -r 'if .ended then (.ended | sub("\.[0-9]+Z$"; "Z") | fromdateiso8601) else (now|floor) end')
START=$((START - 60))
END=$((END + 60))
SCENARIO="ui" # 场景名称;请检查run.ui.exec / k6脚本
2. Search for iteration root spans. TraceQL goes in q
; URL-encode
q2. 搜索迭代根span。TraceQL放在q
中;URL编码。
qit. start
/end
must be unix seconds — RFC3339 returns
startendstart
/end
必须为unix秒——RFC3339格式会返回
startendHTTP 400 invalid start: strconv.ParseUint
. limit=500 caps the
invalid start: strconv.ParseUintHTTP 400 invalid start: strconv.ParseUint
。limit=500限制结果数量,足以覆盖数千次迭代。
invalid start: strconv.ParseUintresult, enough to cover several thousand iterations.
—
Q=$(printf '{ name = "iteration" && span.test.iteration.number >= 0 && span.test.vu >= 0 && span.test.scenario = "%s" }' "$SCENARIO")
QE=$(printf '%s' "$Q" | jq -sRr @uri)
gcx api "/api/plugins/k6-app/resources/logs/api/v1/tempo/api/search?start=${START}&end=${END}&q=${QE}&limit=500"
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/traces_${RUN_ID}.json
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/traces_${RUN_ID}.json
Q=$(printf '{ name = "iteration" && span.test.iteration.number >= 0 && span.test.vu >= 0 && span.test.scenario = "%s" }' "$SCENARIO")
QE=$(printf '%s' "$Q" | jq -sRr @uri)
gcx api "/api/plugins/k6-app/resources/logs/api/v1/tempo/api/search?start=${START}&end=${END}&q=${QE}&limit=500"
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/traces_${RUN_ID}.json
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/traces_${RUN_ID}.json
Each entry has: traceID (32-char hex), rootServiceName ("k6"),
每个条目包含:traceID(32位十六进制)、rootServiceName("k6")、
rootTraceName ("iteration"), durationMs, startTimeUnixNano, and a
rootTraceName("iteration")、durationMs、startTimeUnixNano,以及迭代根span属性的
spanSet preview of the iteration root span's attributes
spanSet预览(test.iteration.number、test.vu、test.scenario)。
(test.iteration.number, test.vu, test.scenario).
3. 获取完整追踪。响应为OTLP格式:
3. Fetch a full trace. The response is OTLP-shaped:
{ "batches": [ { "resource": {...}, "scopeSpans": [ { "spans": [...] } ] } ] }
{ "batches": [ { "resource": {...}, "scopeSpans": [ { "spans": [...] } ] } ] }
—
TRACE_ID=$(jq -r '.traces[0].traceID' /tmp/traces_${RUN_ID}.json)
gcx api "/api/plugins/k6-app/resources/logs/api/v1/tempo/api/traces/$TRACE_ID"
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/trace_${TRACE_ID}.json
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/trace_${TRACE_ID}.json
undefinedTRACE_ID=$(jq -r '.traces[0].traceID' /tmp/traces_${RUN_ID}.json)
gcx api "/api/plugins/k6-app/resources/logs/api/v1/tempo/api/traces/$TRACE_ID"
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/trace_${TRACE_ID}.json
-H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/trace_${TRACE_ID}.json
undefinedCompact summary for an agent
适用于代理的简洁汇总
The OTLP body for a single iteration can run ~100 KB pretty-printed.
Dumping it verbatim into context is wasteful — most of the useful
signal lives in the span-name distribution, the slowest individual
spans, and the web-vital ratings. The pipeline below collapses a
trace into ~1–2 KB of plain text:
bash
jq -r '
# OTel value envelope: pick by key presence, not //-chaining — jq's //
# treats `false` as missing and would drop legitimate boolValue:false.
def attrval:
if has("stringValue") then .stringValue
elif has("intValue") then .intValue
elif has("boolValue") then .boolValue
elif has("doubleValue") then .doubleValue
else null end;
def attrs: [.attributes[] | {(.key): (.value | attrval)}] | add // {};
def dur: (((.endTimeUnixNano|tonumber) - (.startTimeUnixNano|tonumber)) / 1000000 | floor);
[.batches[].scopeSpans[].spans[] | {name, dur: dur, a: attrs, status: (.status.code // "OK")}] as $spans
| "── span-name rollup (count · total ms · max ms) ──",
([$spans[] | {name, dur}]
| group_by(.name) | map({name: .[0].name, n: length,
t: (map(.dur)|add), m: (map(.dur)|max)})
| sort_by(-.t)[]
| " \(.n|tostring|.+" "|.[0:4]) \(.t|tostring|.+" "|.[0:7]) \(.m|tostring|.+" "|.[0:6]) \(.name)"),
"\n── slowest spans (ms · name · navigation.url / page.goto.url) ──",
([$spans[] | select(.name != "iteration")] | sort_by(-.dur) | .[0:8][]
| " \(.dur|tostring|.+" "|.[0:7]) \(.name)\(if .a["navigation.url"] // .a["page.goto.url"] then " " + (.a["navigation.url"] // .a["page.goto.url"]) else "" end)"),
"\n── web-vital ratings ──",
([$spans[] | select(.name == "web_vital")
| {n: .a["web_vital.name"], v: .a["web_vital.value"], r: .a["web_vital.rating"]}]
| group_by(.n) | map({name: .[0].n,
ratings: ([.[].r] | group_by(.) | map({(.[0]): length}) | add)})[]
| " \(.name) \(.ratings)"),
"\n── span errors ──",
([$spans[] | select(.status != "OK" and .status != null)] | length | " count: \(.)")
' /tmp/trace_${TRACE_ID}.jsonOn a run like 7542817 the output is something like:
── span-name rollup (count · total ms · max ms) ──
1 154552 154552 iteration
37 153391 17746 navigation
11 21778 5168 page.waitForNavigation
...
── slowest spans (ms · name · navigation.url / page.goto.url) ──
17746 navigation https://k6e2etests.grafana-dev.net/a/k6-app/projects
14992 navigation https://k6e2etests.grafana-dev.net/a/k6-app/projects/58195
...
── web-vital ratings ──
LCP {"poor":5,"needs-improvement":1}
CLS {"good":2}
...Two paragraphs of text capture which navigations are slow, which
web vitals are regressing, and the overall span-name distribution
— enough to reason about browser performance without paging through
the OTLP tree.
单次迭代的OTLP响应体格式化后约为100 KB。将其直接转储到上下文会浪费资源——大多数有用的信号都存在于span名称分布、最慢的单个span和Web性能指标评级中。下面的管道将追踪数据压缩为约1–2 KB的纯文本:
bash
jq -r '
# OTel值信封:按键存在性选择,而非使用//链式操作——jq的//
# 会将`false`视为缺失,从而丢弃合法的boolValue:false。
def attrval:
if has("stringValue") then .stringValue
elif has("intValue") then .intValue
elif has("boolValue") then .boolValue
elif has("doubleValue") then .doubleValue
else null end;
def attrs: [.attributes[] | {(.key): (.value | attrval)}] | add // {};
def dur: (((.endTimeUnixNano|tonumber) - (.startTimeUnixNano|tonumber)) / 1000000 | floor);
[.batches[].scopeSpans[].spans[] | {name, dur: dur, a: attrs, status: (.status.code // "OK")}] as $spans
| "── span名称汇总(数量 · 总耗时ms · 最大耗时ms) ──",
([$spans[] | {name, dur}]
| group_by(.name) | map({name: .[0].name, n: length,
t: (map(.dur)|add), m: (map(.dur)|max)})
| sort_by(-.t)[]
| " \(.n|tostring|.+" "|.[0:4]) \(.t|tostring|.+" "|.[0:7]) \(.m|tostring|.+" "|.[0:6]) \(.name)"),
"\n── 最慢的span(耗时ms · 名称 · navigation.url / page.goto.url) ──",
([$spans[] | select(.name != "iteration")] | sort_by(-.dur) | .[0:8][]
| " \(.dur|tostring|.+" "|.[0:7]) \(.name)\(if .a["navigation.url"] // .a["page.goto.url"] then " " + (.a["navigation.url"] // .a["page.goto.url"]) else "" end)"),
"\n── Web性能指标评级 ──",
([$spans[] | select(.name == "web_vital")
| {n: .a["web_vital.name"], v: .a["web_vital.value"], r: .a["web_vital.rating"]}]
| group_by(.n) | map({name: .[0].n,
ratings: ([.[].r] | group_by(.) | map({(.[0]): length}) | add)})[]
| " \(.name) \(.ratings)"),
"\n── span错误 ──",
([$spans[] | select(.status != "OK" and .status != null)] | length | " 数量: \(.)")
' /tmp/trace_${TRACE_ID}.json对于运行7542817,输出如下:
── span名称汇总(数量 · 总耗时ms · 最大耗时ms) ──
1 154552 154552 iteration
37 153391 17746 navigation
11 21778 5168 page.waitForNavigation
...
── 最慢的span(耗时ms · 名称 · navigation.url / page.goto.url) ──
17746 navigation https://k6e2etests.grafana-dev.net/a/k6-app/projects
14992 navigation https://k6e2etests.grafana-dev.net/a/k6-app/projects/58195
...
── Web性能指标评级 ──
LCP {"poor":5,"needs-improvement":1}
CLS {"good":2}
...两段文本即可捕获哪些导航较慢、哪些Web性能指标出现退化,以及整体span名称分布——无需翻阅OTLP树即可分析浏览器性能。
Notes worth knowing
值得注意的事项
- Header is mandatory. Both and
/searchreturn HTTP 401 without/traces/<id>. The header scopes the query to the run's tenant.X-K6TestRun-Id - /
startare unix seconds. RFC3339 ISO strings getend. They're technically optional — search works without them — but providing a window matching the run'sHTTP 400 invalid start: strconv.ParseUint/created(plus ~60s buffer) keeps the query fast and avoids matching unrelated runs that share the scenario name.ended - TraceQL, not LogQL. The parameter uses TraceQL — span predicates are written as
qand combined withspan.<attribute>. The suggested query filters iteration root spans for a specific scenario; broaden by dropping&&, narrow by adding e.g.test.scenarioto pinpoint a single iteration on a single VU.span.test.vu = 3 && span.test.iteration.number = 5 - Scenario names come from the run. They're the keys of
on the run object —
.options.scenarioslists them. The default scenario is namedgcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID --json options 2>/dev/null | jq -r '.options.scenarios | keys[]'; browser tests often name itdefault,ui, etc.browser - Span IDs in the OTLP body are base64. The in the search response is hex (the form
traceIDaccepts); the inner/traces/<id>/traceId/spanIdfields inside the OTLP batch are base64-encoded bytes. Cross-correlate parent/child viaparentSpanId, both in base64 — no need to decode.parentSpanId == spanId - Useful attribute keys on browser-test spans: ,
navigation.url,page.goto.url,screenshot.path,web_vital.name,web_vital.value,web_vital.rating,test.scenario,test.vu,test.iteration.number. These are the ones worth surfacing in summaries; the OTel value envelope isk6.test_run_id(the jq{stringValue|intValue|boolValue|doubleValue}helper above handles all four).attrval
- 标头是必填项。和
/search在没有/traces/<id>时都会返回HTTP 401错误。该标头将查询范围限定为运行的租户。X-K6TestRun-Id - **/
start是unix秒。**RFC3339 ISO格式字符串会返回end。它们在技术上是可选的——不设置也可搜索,但提供与运行HTTP 400 invalid start: strconv.ParseUint/created匹配的窗口(加上约60秒缓冲区)可加快查询速度,并避免匹配共享场景名称的无关运行。ended - 使用TraceQL,而非LogQL。参数使用TraceQL——span谓词写为
q,并使用span.<attribute>组合。建议的查询会过滤特定场景的迭代根span;去掉&&可扩大范围,添加例如test.scenario可缩小范围至单个VU上的单次迭代。span.test.vu = 3 && span.test.iteration.number = 5 - **场景名称来自运行对象。**它们是运行对象上的键——
.options.scenarios可列出这些名称。默认场景名为gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID --json options 2>/dev/null | jq -r '.options.scenarios | keys[]';浏览器测试通常命名为default、ui等。browser - **OTLP响应体中的Span ID是base64编码的。**搜索响应中的是十六进制格式(
traceID接受此格式);OTLP批处理中的内部/traces/<id>/traceId/spanId字段是base64编码的字节。通过parentSpanId关联父/子span,两者均为base64编码——无需解码。parentSpanId == spanId - 浏览器测试span上的有用属性键:、
navigation.url、page.goto.url、screenshot.path、web_vital.name、web_vital.value、web_vital.rating、test.scenario、test.vu、test.iteration.number。这些是值得在汇总中展示的属性;OTel值信封格式为k6.test_run_id(上面的jq{stringValue|intValue|boolValue|doubleValue}助手可处理所有四种格式)。attrval
8. Cloud Insights (audit results for a run)
8. Cloud Insights(运行的审计结果)
Cloud Insights runs heuristics against a finished test — checks for
high cardinality, web-vital regressions, missing thresholds,
overutilised load generators, and so on — and exposes the results
through a separate plugin route (not
). gcx still handles auth; no extra header is needed.
/resources/insights//cloud/v{N}/Three calls produce the data, and the agent-useful output is a join
across two of them:
bash
RUN_ID="<run_id>"
BASE="/api/plugins/k6-app/resources/insights/insights/api/v1/testrun/$RUN_ID"Cloud Insights会针对已完成的测试运行启发式检查——检查高基数、Web性能指标退化、缺失阈值、负载生成器过度使用等问题——并通过单独的插件路由公开结果(而非)。gcx仍处理身份验证;无需额外标头。
/resources/insights//cloud/v{N}/三次调用可获取数据,对代理有用的输出是其中两次调用结果的合并:
bash
RUN_ID="<run_id>"
BASE="/api/plugins/k6-app/resources/insights/insights/api/v1/testrun/$RUN_ID"1. List executions for the run, take the most recent. The response
1. 列出运行的执行记录,取最新的一条。响应格式为
is { "executions": [ {id, version}, ... ] } — NOT a bare array.
{ "executions": [ {id, version}, ... ] } —— 不是裸数组。
Pick the last entry; insights re-runs append, and the newest one
选择最后一个条目;insights重新运行时会追加记录,最新的条目
reflects the current set of audits/scores.
反映当前的审计/评分集。
EXEC_ID=$(gcx api "$BASE/executions" -o json 2>/dev/null
| jq -r '.executions[-1].id')
| jq -r '.executions[-1].id')
EXEC_ID=$(gcx api "$BASE/executions" -o json 2>/dev/null
| jq -r '.executions[-1].id')
| jq -r '.executions[-1].id')
2. Audit definitions (id, title, description, weight). ~15 items.
2. 审计定义(id、标题、描述、权重)。约15项。
gcx api "$BASE/executions/$EXEC_ID/audits" -o json 2>/dev/null \
/tmp/insights_audits_${RUN_ID}.json
gcx api "$BASE/executions/$EXEC_ID/audits" -o json 2>/dev/null \
/tmp/insights_audits_${RUN_ID}.json
3. Audit results (audit_id, status, score, explanation, actions).
3. 审计结果(audit_id、状态、评分、说明、操作建议)。
gcx api "$BASE/executions/$EXEC_ID/audits/results" -o json 2>/dev/null \
/tmp/insights_results_${RUN_ID}.json
Both responses use the same top-level key — `audits` — but the
*contents* differ: step 2 is the catalog of what each audit checks,
step 3 is what that audit found on this particular run. Cross-join
them via `result.audit_id == audit.id` (1:1 in practice).gcx api "$BASE/executions/$EXEC_ID/audits/results" -o json 2>/dev/null \
/tmp/insights_results_${RUN_ID}.json
两次响应使用相同的顶级键——`audits`——但*内容*不同:步骤2是每个审计检查内容的目录,步骤3是该审计在此次运行中的发现结果。通过`result.audit_id == audit.id`将两者关联(实际上是1:1关系)。Joining into a compact, agent-readable summary
合并为代理可读的简洁汇总
The raw audit + results JSON together is ~10 KB. Dumping both into
context just to read out three lines per audit is wasteful — do the
join in and emit a single tight block. The pipeline below is
the recommended shape:
jqbash
jq -nr \
--slurpfile a /tmp/insights_audits_${RUN_ID}.json \
--slurpfile r /tmp/insights_results_${RUN_ID}.json '
($a[0].audits | INDEX(.id)) as $defs
| $r[0].audits
| map({
title: ($defs[.audit_id].title // "?"),
description: ($defs[.audit_id].description // ""),
status: .status,
status_reason: .status_reason,
score: (
if .score == null then "n/a"
elif .score.type == "binary" then (if .score.value then "pass" else "fail" end)
else (.score.value | tostring)
end),
explanation: (.explanation // "" | gsub("\n+"; " ") | .[0:300]),
actions: (.actions // [])
})
| .[]
| "── \(.title) — score: \(.score)\(if .status == "failed" then " [audit did not run: \(.status_reason // "unknown")]" else "" end)\n \(.description)\n → \(.explanation)\(if (.actions|length) > 0 then "\n actions:\n - " + (.actions|join("\n - ")) else "" end)\n"
'For a 15-audit run this emits ~5 KB of plain text — title, score,
one-line description, one-line explanation, and any action items.
That's enough for the agent to reason about the test's health
without re-reading either JSON blob.
原始审计+结果JSON合计约10 KB。将两者都转储到上下文只为读取每项审计的三行内容是浪费资源——请使用合并并输出单个紧凑块。推荐使用以下管道格式:
jqbash
jq -nr \
--slurpfile a /tmp/insights_audits_${RUN_ID}.json \
--slurpfile r /tmp/insights_results_${RUN_ID}.json '
($a[0].audits | INDEX(.id)) as $defs
| $r[0].audits
| map({
title: ($defs[.audit_id].title // "?"),
description: ($defs[.audit_id].description // ""),
status: .status,
status_reason: .status_reason,
score: (
if .score == null then "n/a"
elif .score.type == "binary" then (if .score.value then "通过" else "失败" end)
else (.score.value | tostring)
end),
explanation: (.explanation // "" | gsub("\n+"; " ") | .[0:300]),
actions: (.actions // [])
})
| .[]
| "── \(.title) — 评分: \(.score)\(if .status == "failed" then " [审计未运行: \(.status_reason // "未知")]" else "" end)\n \(.description)\n → \(.explanation)\(if (.actions|length) > 0 then "\n 操作建议:\n - " + (.actions|join("\n - ")) else "" end)\n"
'对于包含15项审计的运行,此管道会输出约5 KB的纯文本——标题、评分、一行描述、一行说明以及任何操作建议。这足以让代理分析测试的健康状况,而无需重新读取任何JSON blob。
Notes worth knowing
值得注意的事项
- ≠ verdict.
statusmeans the audit executed. The verdict lives instatus: "succeeded"(scoretrue/false, orbinary0…1 where 1 is best).numericmeans the audit itself could not run (typicallystatus: "failed"— e.g. the HTTP Spans audit on a non-tracing test); thestatus_reason: "missing data"field is absent. Treat these as "no signal", not as failures.score - Score thresholds vary per audit. A 0.94 might be fine for one audit and concerning for another — there's no global cutoff. The
numericis authoritative for what the score means; surface it verbatim rather than inventing a pass/fail rule.explanation - is the actionable bit. Only present when the audit has concrete recommendations (e.g. "Reduce the cardinality of the
actionslabel …"). When summarising a run for a user who's trying to improve it, lead with audits that have a non-emptyurlarray.actions - Pick the last execution, not the first. is in chronological order; re-runs append. Older executions reflect older audit logic and may have stale results.
.executions[] - Insights is a post-run analysis. If the run hasn't finished
(or never produced enough data for insights to compute), the
executions list may be empty — bail out gracefully on
.
length == 0
- ≠ 结论。
status表示审计已执行。结论在status: "succeeded"中(score类型为true/false,binary类型为0…1,1为最佳)。numeric表示审计本身无法运行(通常status: "failed"——例如非追踪测试上的HTTP Span审计);此时status_reason: "missing data"字段不存在。请将这些情况视为“无信号”,而非失败。score - 评分阈值因审计而异。类型的0.94对某些审计来说可能没问题,但对其他审计来说可能值得关注——没有全局阈值。
numeric是评分含义的权威说明;请原样展示,而非自行制定通过/失败规则。explanation - **是可操作的部分。**仅当审计有具体建议时才会存在(例如“降低
actions标签的基数……”)。当为试图改进测试的用户汇总运行结果时,请优先展示url数组非空的审计。actions - 选择最后一次执行记录,而非第一次。按时间顺序排列;重新运行时会追加记录。较早的执行记录反映较旧的审计逻辑,结果可能过时。
.executions[] - **Insights是运行后分析。**如果运行尚未完成(或从未生成足够的数据供insights计算),执行记录列表可能为空——请在时优雅退出。
length == 0
9. Local k6 CLI (smoke tests and k6 cloud run
)
k6 cloud run9. 本地k6 CLI(冒烟测试和k6 cloud run
)
k6 cloud runThe local CLI is the right tool for parse checks and 1-iteration
smoke runs before pushing to cloud:
k6bash
k6 inspect script.js | head -20 # parse-only sanity
k6 run --quiet script.js # run locallyFor (uploads + runs in cloud from your laptop), authenticate
with the token and stack URL pulled from gcx — token
comes from , stack URL from the active gcx context's
field:
k6 cloud runk6 cloud logingcx k6 auth tokengrafana.serverbash
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.jsk6's cloud config () is single-context, so
re-run whenever you switch gcx contexts — otherwise
will keep targeting the previous stack.
~/.config/k6/cloud.jsonk6 cloud logink6 cloud runExit codes: pass, threshold fail, anything else = script/runtime error.
099本地 CLI是推送至云之前进行解析检查和1次迭代冒烟测试的合适工具:
k6bash
k6 inspect script.js | head -20 # 仅解析检查
k6 run --quiet script.js # 本地运行对于(从笔记本电脑上传并在云中运行),使用从gcx获取的令牌和堆栈URL进行身份验证——令牌来自,堆栈URL来自活动gcx上下文的字段:
k6 cloud runk6 cloud logingcx k6 auth tokengrafana.serverbash
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.jsk6的云配置()是单上下文的,因此切换gcx上下文时请重新运行——否则会继续指向之前的堆栈。
~/.config/k6/cloud.jsonk6 cloud logink6 cloud run退出码:表示通过,表示阈值未通过,其他值表示脚本/运行时错误。
09910. Gotchas
10. 常见问题
| Symptom | Cause | Fix |
|---|---|---|
| gcx OAuth session expired | |
| Forgot the doubled | Use |
| Missing | Add it; pass |
| Script PUT returns 200 but doesn't take effect | | Verify by sha256 of GET (§5 step 7) |
Run status | Zero-observation thresholds report as pass; | Add |
| Loki query returns nothing for a recent run | | Always pass |
| Subcommand doesn't follow | Use |
| Fields outside the v6 PATCH schema ( | For project moves use the dedicated |
| The flag is accepted but never filters — | Use |
| The Grafana plugin proxy rewrites multipart Content-Types to | Fall back to direct curl against |
| Missing/wrong | Re-check both headers; resolve |
| Left the plugin-proxy prefix in by mistake | Strip |
| 症状 | 原因 | 解决方法 |
|---|---|---|
| gcx OAuth会话已过期 | |
路径看似正确但返回 | 忘记REST端点的重复 | 使用 |
脚本PUT请求返回 | 缺少 | 添加该标头;向 |
| 脚本PUT请求返回200但未生效 | | 通过GET请求的sha256验证(§5步骤7) |
运行状态为 | 零观测阈值会报告为通过;仅 | 添加 |
| 针对最近运行的Loki查询返回空结果 | 缺少 | 日志查询时始终传递 |
| 子命令不会跟随 | 使用 |
| v6 PATCH模式( | 移动项目请使用专用的 |
| 该标志被接受但从未生效—— | 通过 |
通过 | Grafana插件代理会在转发前将multipart类型的Content-Type重写为 | 退而使用直接curl调用 |
直接调用 | 缺少/错误的 | 重新检查两个标头;通过 |
直接调用 | 误保留了插件代理前缀 | 移除 |
11. Mutations not covered by update -f
update -f11. update -f
未覆盖的变更操作
update -fgcx k6 <resource> update -f✔ Updated <resource> <id>| Mutation | Endpoint | Body |
|---|---|---|
| Move test to another project | | |
| Start a test run | | |
Polling a started run: After starting a run, poll
until reaches or
. Always check the field alongside —
with means a configuration or
infrastructure failure (not a threshold breach). On any non-
result, immediately fetch logs (§4) to surface the error rather than
waiting for the user to report it.
GET /cloud/v6/test_runs/{id}statuscompletedabortedresultstatusstatus: completedresult: errorpassed| Abort a running test | | empty |
| Set / overwrite a schedule | | Schedule body (recurrence_rule or cron) |
| Deactivate / reactivate a schedule | | empty |
| Create a load test (multipart!) | | multipart: + |
| Persist a run past retention | | empty (paired with ) |
POST /cloud/v6/test_runs/{id}/abortPOST /cloud/v6/load_tests/{id}/schedulePOST /cloud/v6/schedules/{id}/{deactivate,activate}POST /cloud/v6/projects/{id}/load_testsnamescriptPOST /cloud/v6/test_runs/{id}/save/unsaveThe PATCH-style updates that do work via :
update -f| Resource | Updatable fields |
|---|---|
| load test | |
| project | |
Both schemas declare — any other field
you put in the manifest is silently filtered out before the PATCH is
sent, even though gcx still reports . Don't try to flip
or move a through ;
they're not exposed for mutation on the v6 PATCH.
additionalProperties: false✔ Updatedis_defaultgrafana_folder_uidupdate -fCross-check by reading the operation's request schema in the OpenAPI
spec (workflow in §3) before assuming a field is mutable.
gcx k6 <resource> update -f✔ Updated <resource> <id>| 变更操作 | 端点 | 请求体 |
|---|---|---|
| 将测试移动到另一个项目 | | |
| 启动测试运行 | | |
**轮询已启动的运行:**启动运行后,轮询直到变为或。请始终同时检查字段和——且表示配置或基础设施故障(而非阈值未通过)。对于任何非的结果,请立即获取日志(§4)以暴露错误,而非等待用户报告。
GET /cloud/v6/test_runs/{id}statuscompletedabortedresultstatusstatus: completedresult: errorpassed| 中止正在运行的测试 | | 空请求体 |
| 设置/覆盖调度 | | 调度请求体(recurrence_rule或cron) |
| 停用/重新激活调度 | | 空请求体 |
| 创建负载测试(multipart格式!) | | multipart格式: + |
| 将运行保留超过保留期 | | 空请求体(与配合使用) |
POST /cloud/v6/test_runs/{id}/abortPOST /cloud/v6/load_tests/{id}/schedulePOST /cloud/v6/schedules/{id}/{deactivate,activate}POST /cloud/v6/projects/{id}/load_testsnamescriptPOST /cloud/v6/test_runs/{id}/save/unsave可通过实现的PATCH式更新:
update -f| 资源 | 可更新字段 |
|---|---|
| 负载测试 | |
| 项目 | 仅 |
两种模式都声明——您在清单中添加的任何其他字段都会在发送PATCH前被静默过滤,即使gcx仍会报告。请勿尝试通过修改或移动;它们未在v6 PATCH中暴露为可修改字段。
additionalProperties: false✔ Updatedupdate -fis_defaultgrafana_folder_uid在假设字段可修改之前,请通过OpenAPI规范中的操作请求模式交叉检查(§3中的工作流程)。
Worked example — move a test between projects
示例——在项目之间移动测试
bash
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>}'bash
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>}'Verify — re-GET and check project_id reflects the new value.
验证——重新GET并检查project_id是否已更新为新值。
Do not trust the absence of an error from the PUT; HTTP 204 with
请勿依赖PUT请求无错误;HTTP 204且响应体为空是成功的标志,但gcx api不会输出任何内容。
empty body is the success shape but gcx api prints nothing.
—
gcx --context <ctx> k6 load-tests get <test_id> -o json | jq '{id, name, project_id}'
The OpenAPI description for this endpoint is explicit: *"Move a load
test to a different project of the same organization. All respective
test runs will be also moved to the new project."* You don't need to
migrate runs separately.gcx --context <ctx> k6 load-tests get <test_id> -o json | jq '{id, name, project_id}'
该端点的OpenAPI描述明确说明:*"将负载测试移动到同一组织的不同项目。所有相关的测试运行也会被移动到新项目中。"*您无需单独迁移运行记录。Cascade behavior worth knowing
值得了解的级联行为
- Deleting a load test cascade-deletes its schedule. The schedule
is gone from and
/cloud/v6/schedulesimmediately. No need to/cloud/v6/load_tests/{id}/schedulefirst as a defensive step.gcx k6 schedules delete <load-test-id> - Deleting a project with a running test fails with HTTP 409
("Cannot delete project with a running test." per the OpenAPI
spec). Non-running tests appear to be removed with the project,
but if you want to inventory a project's contents before deleting
it, use (NOT
GET /cloud/v6/projects/{id}/load_tests, which doesn't filter — see §10).gcx k6 load-tests list --project-id <id> - Moving a test moves its runs and run history with it. Schedule
attachment also follows the test (it's keyed by , not by project).
load_test_id
- **删除负载测试会级联删除其调度。**调度会立即从和
/cloud/v6/schedules中消失。无需先执行/cloud/v6/load_tests/{id}/schedule作为防御步骤。gcx k6 schedules delete <load-test-id> - 删除包含正在运行测试的项目会返回HTTP 409错误(根据OpenAPI规范:"Cannot delete project with a running test.")。非运行中的测试似乎会随项目一起被删除,但如果您想在删除项目前清点其内容,请使用(而非
GET /cloud/v6/projects/{id}/load_tests,该命令不会过滤——请参阅§10)。gcx k6 load-tests list --project-id <id> - **移动测试会同时移动其运行记录和历史。**调度关联也会跟随测试(它以为键,而非项目)。
load_test_id
Verification rule of thumb
验证经验法则
The recurring pattern in this skill is "gcx confirms success even
when the underlying call no-ops". Whenever you mutate state:
- Note what you expected to change (field, count, status).
- Re-GET the resource and confirm the change is reflected.
- If it isn't, check whether the mutation needed a dedicated
endpoint (this section) rather than .
update -f
本技能中反复出现的模式是*"gcx确认成功,但底层调用实际未生效"*。每当您修改状态时:
- 记录您期望变更的内容(字段、计数、状态)。
- 重新GET资源并确认变更已生效。
- 如果未生效,请检查该变更是否需要专用端点(本节内容)而非。
update -f