Repository navigation
Stream --local-execution logs to Grafana Cloud - #6171
Conversation
76aedf3 to
5910166
Compare
5910166 to
81ecc4e
Compare
81ecc4e to
64599be
Compare
64599be to
cff82e9
Compare
cff82e9 to
f3cb7d6
Compare
inancgumus
left a comment
There was a problem hiding this comment.
Solid work 👍 Two real issues in the shutdown path, the rest are questions and nits.
| case e := <-p.buf: | ||
| forward(e) | ||
| default: | ||
| lokiCancel() |
There was a problem hiding this comment.
finish() empties p.buf into the loki hook's channel, then cancels the loki context right away. But lokiHook.Listen returns on cancel without draining h.ch, so the final flush never sends what's still queued there. Fire 500 entries, call Drain, and the backend receives 0 of them. Can you add a test for this? TestPusher_Drain misses it because it waits for a periodic push before draining.
There was a problem hiding this comment.
Good catch — this is a real gap. The loss is in the loki hook's shutdown path: lokiHook.Listen's <-ctx.Done() branch flushes only the already-consumed msgs and never drains what's still queued in h.ch. That behaviour predates this PR — it's the loki hook's long-standing shutdown handling (unchanged here) and is shared with --out loki; what's new is this PR's Drain() path, which is what makes it observable for cloud logs.
Since the proper fix belongs in the shared loki hook (and would fix --out loki's final-batch loss too), I'd rather not widen this PR with it and will handle it in a dedicated follow-up, including the regression test you described (Fire N with a long push period so nothing flushes periodically, Drain, assert the backend received all N).
If you'd prefer we don't ship the new Drain path with that known tail-loss, I can instead add a contained fix in the cloudlog layer here — happy to do whichever you'd rather. WDYT?
There was a problem hiding this comment.
A follow-up works for me 👍
| minLevel = lvl | ||
| } else if p.fallbackLogger != nil { | ||
| p.fallbackLogger.WithField("level", c.Level). | ||
| Warn("invalid cloud log level; pushing all levels") |
There was a problem hiding this comment.
This warn never fires. NewLokiHook already fails on the same invalid level, and Listen returns at the error branch above. So one bad logs.level from the backend turns off the whole log push instead of falling back to all levels.
There was a problem hiding this comment.
Good catch — fixed in 97aebc5c. You're right on both counts: NewLokiHook fails on the bad level and Listen returns at the error branch, so one bad logs.level disabled the whole push, and the fallback warn below was unreachable.
Root cause was passing Level to NewLokiHook at all — the pusher already filters by level itself in forward (the hook's Levels() dispatch is bypassed, since we call Fire directly), so it was redundant and the only thing turning a bad value into a hard failure. I dropped it and now interpret the level in the pusher:
- valid → that level
- invalid → fall back to info (the backend's own default) + a live warning
- unset → keep all / defer to the logger (unchanged)
I chose info rather than "all" for the invalid case so a bad backend value can't start streaming debug to the cloud under -v — matching how --log-output=loki=…,level=info behaves. Added TestPusher_InvalidLevelDefaultsToInfo.
| if opts.PushPeriod != 0 { | ||
| h.pushPeriod = opts.PushPeriod | ||
| } | ||
| if opts.Limit != 0 { |
There was a problem hiding this comment.
A negative Limit gets past this check and panics in lokiHook.Listen at make([]tmpMsg, h.limit). A negative PushPeriod does the same in time.NewTicker. Nothing recovers in that goroutine, so the panic takes down the whole run. Both can come in through K6_CLOUD_LOGS_LIMIT=-5 / K6_CLOUD_LOGS_PUSH_PERIOD=-1s or a negative backend logs.limit, while the parser for the config line still rejects them.
There was a problem hiding this comment.
Fixed in 53653a94. Non-positive Limit, PushPeriod and MsgMaxSize now clamp to the built-in defaults (with a warn) in NewLokiHook, instead of reaching make([]tmpMsg, limit) / time.NewTicker(pushPeriod) — or the message-size slicing on push — and panicking in that goroutine.
I hardened MsgMaxSize alongside the two you flagged, since a negative there panics the same way on the push path. The config-line path is unchanged — parseArgs still rejects limit < 1 etc. up front — so this only affects the programmatic/backend inputs (K6_CLOUD_LOGS_* / runtime_config.logs). Added TestNewLokiHookClampsNonPositiveOptions.
| // logsPushPath is the path the local-execution mocks use for the cloud | ||
| // logs push endpoint, overriding the real host baked into | ||
| // DefaultStartLocalExecutionResponse so tests never contact it. | ||
| const logsPushPath = "/logs/v1/push" |
There was a problem hiding this comment.
setupSelfProvisionedRun in cmd_cloud_run_push_credentials_test.go doesn't override the logs URL, so its five subtests now push logs to the real https://logs.k6.io:
level=error msg="Error while sending logs to loki" error="Post \"https://logs.k6.io\": dial tcp: lookup logs.k6.io: no such host"
On a CI machine with network access, that reaches production.
There was a problem hiding this comment.
Fixed in 2ba2311b. setupSelfProvisionedRun now points the logs push URL at the mock (srv.URL + logsPushPath), mirroring how it already redirects the metrics URL — so the five self-provisioned subtests no longer egress to the real logs.k6.io (verified: those egress lines drop 5 → 0).
| t.Parallel() | ||
|
|
||
| ts := makeTestState(t, script, []string{"--local-execution", "--no-cloud-logs"}) | ||
| setupLocalExecutionProvMock(t, ts) |
There was a problem hiding this comment.
Should this also assert that no push reached the server? The helper already returns the recorder, and the nil check only covers the registration.
There was a problem hiding this comment.
Done in 83bc1775. Captured the recorder and added assert.Empty(t, bodies, …) after the nil check, so the test now also asserts no log push reached the mock — the opt-out is guaranteed end-to-end, not just at registration.
| }, got) | ||
| } | ||
|
|
||
| func TestBuildConfigFromRuntimeConfig_NilFieldsLeftUnset(t *testing.T) { |
There was a problem hiding this comment.
Can you cover the Logs fields here too? Nothing checks that they stay unset when provisioning omits them, and in that case an env K6_CLOUD_LOGS_PUSH_URL survives Apply and configures the pusher.
There was a problem hiding this comment.
Done in 0a105cb — the test now checks the log fields stay unset when provisioning omits them, and that a stray env var doesn't leak in. See the config thread for the actual fix (dropped the env binding).
| // the cloud log drainer is flushed before the run is notified complete, so | ||
| // in-run logs land while the run is still open (afterwards the backend | ||
| // rejects late pushes). | ||
| func TestOutputStopWithTestError_DrainsLogsBeforeNotify(t *testing.T) { |
There was a problem hiding this comment.
Do we have a test that the drain also runs in the PushRefID path? If a later change ties the drain to notify, the externally provisioned flow loses its final batch and no test fails.
There was a problem hiding this comment.
Good shout — added one in 6eb028c. The existing drain test only covered the self-provisioned path (where notify runs anyway); the new one checks the drain still runs on the PushRefID path, where notify is skipped. So if the drain ever gets tied to notify, that flow won't silently lose its last batch.
| // complete the backend rejects late log pushes, so the final batch would | ||
| // be lost. Best-effort and bounded by the configured request timeout. | ||
| if out.logDrainer != nil { | ||
| dctx, cancel := context.WithTimeout(context.Background(), out.config.Timeout.TimeDuration()) |
There was a problem hiding this comment.
What happens with K6_CLOUD_TIMEOUT=0? TimeDuration() ignores Valid, so this context expires immediately and notify runs while the flush is still going. The other cloud clients treat the same 0 as no timeout.
There was a problem hiding this comment.
Good catch — real bug. Fixed in a6d431f: the drain no longer bounds itself with K6_CLOUD_TIMEOUT — the push is already bounded by the loki hook's own client timeout, so it just waits for the flush on a fresh context. That removes both the instant-expiry at 0 and the mismatched timeout, so notify no longer races the flush.
| // by the provisioning API's runtime_config.logs (self-provisioned) or | ||
| // by an external orchestrator via env (externally-provisioned). Consumed | ||
| // by cmd to configure the cloud log pusher; env-only, not serialised. | ||
| LogsPushURL null.String `json:"-" envconfig:"K6_CLOUD_LOGS_PUSH_URL"` |
There was a problem hiding this comment.
Why do these get envconfig tags when MetricsPushURL and TestRunToken above deliberately don't? A stray env value survives Apply whenever the backend sends an empty value, e.g. K6_CLOUD_LOGS_ALLOWED_LABELS overrides a legitimate allowed_labels: [].
There was a problem hiding this comment.
Good question — they shouldn't have. Fixed in 0a105cb: self-provisioned runs now take the log config only from the provisioning response, and only externally-provisioned runs read the env vars. So a stray env var can't quietly override a self-provisioned run anymore. They were all new in this PR, so nothing relied on the old behaviour.
|
|
||
| // LogsConfig holds the log-push configuration from the provisioning | ||
| // API's runtime_config.logs object. | ||
| type LogsConfig struct { |
There was a problem hiding this comment.
Forgotten, or not needed for local execution? tail_url is the only SDK logs field that isn't mapped.
There was a problem hiding this comment.
Not needed for local execution — tail_url is for tailing logs from the cloud (the live stream you get during a normal k6 cloud run), and --local-execution only pushes logs, it never tails. So it's deliberately skipped; added a note on LogsConfig to make that clear.
The cloud log drain runs before notify, but the only test that exercised it used the self-provisioned path, where notify also runs. Add a test that the drain runs on the external (PushRefID) path too, where notify is skipped, so a future change tying the drain to notify can't silently drop the final batch.
The log drain wrapped the flush in context.WithTimeout using
conf.Timeout — the cloud API request timeout, which the log path
doesn't otherwise use and which every other client applies via
http.Client{Timeout} (where 0 means "no timeout"). As a context
deadline, K6_CLOUD_TIMEOUT=0 expired immediately, so the drain
was skipped and notify raced the flush.
The push is already bounded by the loki hook's own HTTP client
timeout, so drain on a plain fresh context and let that govern.
Add a test that the drain isn't handed an already-expired
context.
tail_url is for tailing logs from the cloud (the live stream in a normal k6 cloud run); --local-execution only pushes logs, so it is deliberately not mapped into LogsConfig. Note it on the type so it doesn't read as an oversight.
The cloud log pusher observes entries only after the secrets-redaction hook, so a logged secret should reach the backend only in redacted form. Nothing tested it — a regression that let secrets through to cloud logs would pass CI. Add an e2e: a VU logs a secret from a mock source, and the pushed body must carry ***SECRET_REDACTED***, never the raw value.
af24d52 to
0f4ff23
Compare
An explicitly empty K6_CLOUD_LOGS_ALLOWED_LABELS decodes to a non-nil empty slice. The loki hook treats nil as "keep all labels" but a non-nil empty slice as "keep none", which stripped the required test_run_id label and got the cloud log push rejected with 401. Normalise an empty allow-list to nil so the keep-all path is taken and test_run_id always survives. Covered by a new unit test and an end-to-end case that sets K6_CLOUD_LOGS_ALLOWED_LABELS="".
--log-output=loki= (an empty URL) is rejected up front by the config parser rather than silently falling back to the default endpoint, so no empty address ever reaches the constructor. Bare loki still uses the default. This is unchanged from before the loki hook constructor extraction, since parseArgs still runs strvals.Parse first. Add a test documenting both cases.
| // so it flushes buffered logs before the run is notified complete. The | ||
| // concrete-nil guard avoids handing the output a typed-nil drainer once it is | ||
| // wrapped in an interface. | ||
| func attachCloudLogDrainer(out output.Output, gs *state.GlobalState) { |
There was a problem hiding this comment.
Nit: having this as a function called unconditionally in the output construction loop implies to me that every output gets the log drainer. Only when you check the function implementation you learn it's restricted to the cloud output.
I would get rid of the function and place the whole if into the loop, just like the others. Makes the intent clearer IMO.
| // into the serialized cloud config that the Output reads. It also reads the | ||
| // K6_CLOUD_LOGS_* log-push config, which the external flow supplies the same | ||
| // way. | ||
| func applyExternalProvisioningCreds( |
There was a problem hiding this comment.
Nit: Maybe rename this func to something like applyExternalProvisioningConfig because it no longer only applies the credentials.
|
|
||
| // bufferCap bounds the pre-configuration entry buffer. It matches the loki | ||
| // hook's own channel capacity so Fire stays non-blocking under the same load. | ||
| const bufferCap = 1000 |
There was a problem hiding this comment.
If this is supposed to mirror the capacity of the loki hook, it could make sense to introduce a shared constant so we make sure that these values do not drift apart in the future
| if lvl, err := logrus.ParseLevel(c.Level); err == nil { | ||
| minLevel = lvl | ||
| } else { | ||
| // The level comes from the backend, not the user, so a bad value |
There was a problem hiding this comment.
this comment is somewhat inaccurate. the level can also come as an env var when we are in the externally provisioned flow. Outcome is probably still the same but I think its worth to fix the comment :)
| // explicitly empty K6_CLOUD_LOGS_ALLOWED_LABELS decodes to a non-nil []string{}, | ||
| // which must still keep the required test_run_id label rather than strip every | ||
| // label from the stream. | ||
| func TestPusher_EmptyAllowedLabelsKeepsTestRunID(t *testing.T) { |
There was a problem hiding this comment.
i think this is sufficiently covered by the cmd level integration tests. maybe we could drop it
| // with Level "info" the pusher pushes info/warn/error entries but filters out | ||
| // the less-severe debug entry, even though the loki hook's Fire enqueues | ||
| // unconditionally. Entries reach the backend via the periodic push. | ||
| func TestPusher_FiltersBelowConfiguredLevel(t *testing.T) { |
There was a problem hiding this comment.
You could merge this with the following test. this would remove a lot of boilerplate and would also improve TestPusher_EmptyLevelKeepsAll because it will actually send logs on all levels.
|
@janHildebrandt98 thank you for the review and feedback! I will action on the comments in a new PR 🙇 |
Brings the branch up to date with master (5880ca9) and resolves the overlap with the cloud changes that landed in the meantime: - grafana#6142 added `k6 cloud load-zone`, whose constructor took the `*cmdCloud` this branch removes. `getCmdCloudLoadZone` now takes a `*state.GlobalState`, like the `project` and `test` constructors already do here. - grafana#6208 dropped the run flag set from the parent `k6 cloud` command and pinned that with a test. This branch had bound those flags to throwaway values to keep the help output unchanged, which is now both unnecessary and wrong, so that block is gone. - grafana#6171 added `--no-cloud-logs`. Its validation stays in `cmdCloudRun.preRun`, followed by `applyCloudEnvOverrides` instead of the parent's `preRun`. - master's new `newCloudV6ClientFromConfig` and `cloudStackName` helpers are kept as they are. No behaviour change beyond what the merged commits already do.
k6 2.2.0 Created-by: HarmonybrewBot Commit-by: HarmonybrewBot Merged-by: HarmonybrewBot Description: Created by `brew bump` --- Created with `brew bump-formula-pr`.<details> <summary>release notes</summary> <pre>k6 `v2.2.0` is here 🎉! This release includes: - `k6 cloud run --local-execution` now streams k6's logs to Grafana Cloud, so the test run's log view works for local execution too. - `chromium.connectOverCDP()`, which connects browser tests to an already-running Chromium instance. - `TextEncoder` and `TextDecoder` available as globals, and `WritableStream` support in `k6/experimental/streams`. - A `k6 cloud load-zone list` command. - Two new experimental feature flags: `merge-run-tags` and `freeze-env`. ## Breaking changes There are no breaking changes in this release. ## New features ### `k6 cloud run --local-execution` streams logs to Grafana Cloud [#6171](grafana/k6#6171) When a cloud test runs locally with `k6 cloud run --local-execution`, k6's logs now stream to the Grafana Cloud test run, so the run's log view is populated the same way it is for cloud execution. Previously, local-execution logs stayed on the machine running k6 and never reached the cloud. Work with Grafana's secrets management to safely work with secrets and redact them if they're accidentally leaked into logs are pushed. Use the new `--no-cloud-logs` flag opts out to opt out of streaming of logs when working with `--local-execution`: ```shell k6 cloud run --local-execution script.js k6 cloud run --local-execution --no-cloud-logs script.js ``` ### Connect to a running browser with `chromium.connectOverCDP()` [#6165](grafana/k6#6165) The browser module can now attach to an existing Chromium-based browser over the Chrome DevTools Protocol, mirroring Playwright's `browserType.connectOverCDP()`. Pass the browser's WebSocket endpoint and k6 manages the returned browser's connection — it's auto-closed at the end of the iteration, though you can call `close()` earlier to release the connection on demand. ```js import { chromium } from 'k6/browser'; export default async function () { const browser = await chromium.connectOverCDP('ws://localhost:9222/devtools/browser/<id>'); const page = await browser.newPage(); try { await page.goto('https://quickpizza.grafana.com/'); } finally { await page.close(); await browser.close(); } } ``` Unlike the `K6_BROWSER_WS_URL` environment variable, the endpoint is a runtime value — you can, for example, request a fresh session URL from a browser provider's API in `setup()` and connect to it from the iterations. ### `TextEncoder` and `TextDecoder` globals [#6182](grafana/k6#6182) `TextEncoder` and `TextDecoder` are now available as standard globals in both the init and VU contexts, no import required — matching how they are exposed in browsers and other JavaScript runtimes. ```js const encoded = new TextEncoder().encode('Hello, world!'); const decoded = new TextDecoder().decode(encoded); ``` ### `WritableStream` in `k6/experimental/streams` [#6132](grafana/k6#6132) The experimental streams module now implements `WritableStream` and `WritableStreamDefaultWriter` following the WHATWG Streams specification, complementing the existing `ReadableStream` and paving the way for a future `TransformStream` implementation. ```js import { WritableStream } from 'k6/experimental/streams'; export default async function () { const stream = new WritableStream({ write(chunk) { console.log(`wrote ${chunk}`); }, }); const writer = stream.getWriter(); await writer.write('hello'); await writer.close(); } ``` ### `k6 cloud load-zone list` command [#6142](grafana/k6#6142) A new `k6 cloud load-zone list` subcommand lists the load zones — public and private — available in the configured Grafana Cloud k6 stack, mirroring the existing `k6 cloud project list` command. Output defaults to a human-readable table; pass `--json` to emit a JSON array instead. ```console $ k6 cloud load-zone list Load zones for https://example.grafana.net: ID NAME TYPE AVAILABLE amazon:us:ashburn Ashburn, US (Amazon) public yes amazon:sa:cape town Cape Town, SA (Amazon) public yes ``` ### Configurable `handleSummary()` timeout [#5854](grafana/k6#5854) The time budget for the `handleSummary()` callback — previously hardcoded to 120 seconds — is now configurable through the `handleSummaryTimeout` option or the `K6_HANDLE_SUMMARY_TIMEOUT` environment variable, so long-running tests with heavy summaries no longer fail with `handleSummary() execution timed out`. Thanks, @LBaronceli! ```js export const options = { handleSummaryTimeout: '5m', }; ``` ### New experimental feature flags: `merge-run-tags` and `freeze-env` Two new experimental flags join the feature-flag system introduced in v2.1.0: - [#5714](grafana/k6#5714) `merge-run-tags` merges run tags per key across config layers, so `options.tags` in a script is no longer silently discarded when `--tag` or `K6_TAGS` is also used — higher-priority layers win on conflicting keys instead of replacing the whole map. Thanks, @yordis! - [#6032](grafana/k6#6032) `freeze-env` freezes the `__ENV` object, so modifications from script code throw a `TypeError` (in strict mode) instead of silently persisting across iterations and scenarios. Thanks, @lohitkolluri! ```shell k6 run --features merge-run-tags,freeze-env script.js ``` ## UX improvements and enhancements - [#5631](grafana/k6#5631) Makes the browser module's header accessors — `response.allHeaders()`, `headerValue()`, `headerValues()`, and `headersArray()` — return the raw wire headers (including `Set-Cookie` and security-related headers), correctly paired with each hop of a redirect chain instead of Chrome's provisional headers. As part of this, `headerValues()` now matches header names case-insensitively and splits repeated values on newlines rather than commas, and the `browser_data_sent`/`browser_data_received` metrics now include the raw header bytes and no longer vary run-to-run with CDP event ordering. - [#6208](grafana/k6#6208) Makes `k6 cloud` reject the run flags (for example, `--vus`) with an `unknown flag` error and a non-zero exit code. Previously `k6 cloud --vus 10 script.js` accepted the flags, printed the help text, and exited 0 — running tests with `k6 cloud` directly was deprecated in v2.0.0 in favor of `k6 cloud run`. - [#6096](grafana/k6#6096) Points the cloud secrets error at `K6_CLOUD_SECRETS_TOKEN` and `K6_CLOUD_SECRETS_ENDPOINT` when a test run is reused via `K6_CLOUD_PUSH_REF_ID` under `--local-execution`, instead of suggesting the `--local-execution` flag the user is already using. - [#6196](grafana/k6#6196) Adds `catch` blocks to the browser examples so a failing iteration reports the original error instead of a subsequent `page.close()` failure. Thanks, @locker95! ## Bug fixes - [#6234](grafana/k6#6234) Classifies HTTP/2 errors by message so the `error_code` metric tag stays correct when k6 is built with Go 1.27 (whose `x/net/http2` delegates to the standard library), and explicitly enables HTTP/2 negotiation on VU transports. - [#6232](grafana/k6#6232) Drains queued log entries in the Loki hook at shutdown so `--out loki` and cloud log streaming no longer lose the final batch, and emits a `k6 dropped N log messages` warning when the cloud log buffer overflows instead of dropping logs silently. - [#6125](grafana/k6#6125) Serializes the first concurrent open of a file in the caching filesystem so parallel `fs.open()` calls on the same file no longer read zero or truncated bytes. - [#6147](grafana/k6#6147) Fixes a data race and inconsistent request-interception state when browser routes are added or removed concurrently. Thanks, @somak2kai! - [#6070](grafana/k6#6070) Flushes buffered file log output once per second so recent logs aren't lost when k6 is killed before shutdown. Thanks, @rohan-patnaik! - [#6205](grafana/k6#6205) Stops sending an invalid `Sec-WebSocket-Protocol` header when tailing Grafana Cloud logs; spec-strict servers rejected the handshake with `websocket: bad handshake`. - [#6200](grafana/k6#6200) Leaves a counter's `rate` unset when the observed duration is zero, instead of computing `+Inf` and spuriously failing `rate` thresholds. Thanks, @samarth70! - [#6195](grafana/k6#6195) Initializes a gauge's maximum from the first sample so all-negative gauge series no longer report `max=0`. Thanks, @Solaris-star! - [#6145](grafana/k6#6145) Prevents the OpenTelemetry output from panicking at startup when basic auth is configured without `K6_OTEL_HEADERS`. Thanks, @lukdz! - [#6140](grafana/k6#6140) Stops `SharedArray` deep-freezing JS primitives, which needlessly wrapped large strings in `String` objects — cutting memory usage in the reported reproduction from roughly 1 GB to 100 MB. ## Maintenance and internal improvements - [#6126](grafana/k6#6126), [#6224](grafana/k6#6224), [#6229](grafana/k6#6229) Adds anonymous extension usage to the k6 usage report: a run reports the Go module path, version, and type of registry-cataloged extensions it actually uses (imported `k6/x/` modules, output extensions selected with `--out`, and `k6 x` subcommands). Private and unlisted extensions are never reported, and the existing `--no-usage-report` opt-out covers it. - [#6183](grafana/k6#6183), [#6218](grafana/k6#6218) Updates Sobek and regexp2, making `W See merge request: Harmonybrew/homebrew-core!16299
What?
Stacked on #6170 (external-provisioning), now on
masterafter the provisioning migration (#6169) merged. Streamk6 cloud run --local-executionlogs to Grafana Cloud k6, using the logs configuration the provisioning API already returns fromstart_local_execution(runtime_config.logs) but which was decoded and then dropped.The flow:
runtime_config.logs(push URL, level, limit, push period, message max size, allowed labels) into the provisioning response instead of dropping it.internal/logloki hook — is auto-registered on the logger for--local-execution(unless--no-cloud-logs) and buffers entries until it is configured (the push URL/token are only known after provisioning).push_urlwith the scopedtest_run_tokenasAuthorization: Bearerand the requiredtest_run_idstream label.--verbosestill honours alevel=infodirective for the cloud; local output is unaffected).Notable pieces:
internal/log/cloudpackage (the pusher). It reuses the loki hook via a new programmatic constructorlog.NewLokiHook(extracted from the existing config-line parser, no behaviour change) — no reimplementation of Loki batching/format.cloudapi.Configgains env-only (json:"-")K6_CLOUD_LOGS_*fields, mirroringLogsTailURL, for the externally-provisioned case.--no-cloud-logsopts out (mirrors--no-cloud-secrets).k6 run --out cloud(path B) and managed cloud execution's--show-logstail path are unchanged — no client-side log push there. Eight atomic commits, each individually buildable + lintable + green.Why?
For
--local-executionthe test runs on the client, so its logs are produced locally and never reached the cloud run's log view (only managed cloud execution had server-side logs, tailed via--show-logs). This closes that gap — local-execution logs now stream to the run, matching how metrics were migrated. The externally-provisioned path lets an orchestrator that provisioned the run itself hand k6 the logs config via env.Checklist
make check) and all pass.Related PR(s)/Issue(s)
local-exec-prov-externaltolocal-exec-prov-external-take3and adapted to the SDK'sint64resource IDs.local-exec-prov-external-take3, so this PR's diff is only the log-streaming increment. The migration Migrate k6 cloud run --local-execution to provisioning API #6169 is already merged intomaster; review Support externally-provisioned runs in cloud run --local-execution #6170 first, and retarget this PR tomasteronce Support externally-provisioned runs in cloud run --local-execution #6170 merges.--no-cloud-logs,--out cloud(no push), externally-provisioned (env creds), and secrets-redaction. Real testing found and fixed two issues now in the branch: the final log batch 403'd because it flushed after notify (fixed by draining before notify), and the backend log level wasn't honoured under--verbose(fixed by filtering in the pusher).