@norskvideo/ctl-test-harness 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,16 @@ export interface LogHit {
32
32
  container: string;
33
33
  line: string;
34
34
  }
35
+ /** Copy every engine `logs` tree under an instance-runtime root to `dest`,
36
+ * preserving the relative structure. Why this and not `docker logs`:
37
+ * norsk-ctl bind-mounts each media container's `/var/log/norsk` to
38
+ * `<runnerDir>/logs/media` on the host (override-generator), so the FULL
39
+ * structured engine log (`debug.json`, every level — not the notice+ subset
40
+ * `docker logs` sees, and immune to a decode-error stdout flood drowning the
41
+ * tail) lives on disk there until teardown nukes the store. Call this BEFORE
42
+ * cleanup to keep those logs for a CI artifact. Best-effort: never throws;
43
+ * returns the number of `logs` trees copied. */
44
+ export declare function preserveEngineLogs(runtimeRoot: string, dest: string): number;
35
45
  /** A string forbids a substring; a RegExp is tested per line. */
36
46
  export type LogPattern = string | RegExp;
37
47
  /** Every line, in any container of the instance, that matches a forbidden
package/container-logs.js CHANGED
@@ -8,6 +8,8 @@
8
8
  // Pure argv/parse/format helpers; only the two exported probes shell out,
9
9
  // through an injectable spawn.
10
10
  import { spawnSync } from "node:child_process";
11
+ import { cpSync, existsSync, readdirSync, statSync } from "node:fs";
12
+ import { join, relative } from "node:path";
11
13
  const defaultSpawn = (cmd, args, opts) => {
12
14
  const r = spawnSync(cmd, args, opts);
13
15
  return {
@@ -16,7 +18,14 @@ const defaultSpawn = (cmd, args, opts) => {
16
18
  ...(r.error ? { error: r.error } : {}),
17
19
  };
18
20
  };
19
- const DEFAULT_TAIL = 80;
21
+ // The diagnostic dump is a post-mortem: a container that has already failed,
22
+ // whose logs die with it at teardown. 80 lines rarely reached back to the
23
+ // cause (a ladder-node death, an nvenc failure, the FRC/surface errors that
24
+ // precede it) — the tail is the only copy, so bias it wide. The media engine
25
+ // log is the verbose one; a few thousand lines is cheap in an uploaded
26
+ // artifact and routinely the difference between a diagnosable run and a
27
+ // re-run.
28
+ const DEFAULT_TAIL = 3000;
20
29
  /** `docker ps` argv listing "<name>\t<state>" for ONE instance's containers. */
21
30
  export function instanceContainersArgs(instanceId) {
22
31
  return ["ps", "-a", "--filter", `label=norsk-ctl.instance=${instanceId}`, "--format", "{{.Names}}\t{{.State}}"];
@@ -85,6 +94,54 @@ export function collectInstanceContainerLogs(instanceId, tail = DEFAULT_TAIL, sp
85
94
  export function dumpInstanceContainerLogs(instanceId, tail = DEFAULT_TAIL, spawn = defaultSpawn) {
86
95
  return formatContainerLogs(instanceId, collectInstanceContainerLogs(instanceId, tail, spawn));
87
96
  }
97
+ /** Copy every engine `logs` tree under an instance-runtime root to `dest`,
98
+ * preserving the relative structure. Why this and not `docker logs`:
99
+ * norsk-ctl bind-mounts each media container's `/var/log/norsk` to
100
+ * `<runnerDir>/logs/media` on the host (override-generator), so the FULL
101
+ * structured engine log (`debug.json`, every level — not the notice+ subset
102
+ * `docker logs` sees, and immune to a decode-error stdout flood drowning the
103
+ * tail) lives on disk there until teardown nukes the store. Call this BEFORE
104
+ * cleanup to keep those logs for a CI artifact. Best-effort: never throws;
105
+ * returns the number of `logs` trees copied. */
106
+ export function preserveEngineLogs(runtimeRoot, dest) {
107
+ if (!existsSync(runtimeRoot))
108
+ return 0;
109
+ let copied = 0;
110
+ const walk = (dir) => {
111
+ let names;
112
+ try {
113
+ names = readdirSync(dir);
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ for (const name of names) {
119
+ const p = join(dir, name);
120
+ try {
121
+ if (!statSync(p).isDirectory())
122
+ continue;
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ if (name === "logs") {
128
+ // Copy the whole tree wholesale; don't descend further.
129
+ try {
130
+ cpSync(p, join(dest, relative(runtimeRoot, p)), { recursive: true });
131
+ copied += 1;
132
+ }
133
+ catch {
134
+ /* best-effort */
135
+ }
136
+ }
137
+ else {
138
+ walk(p);
139
+ }
140
+ }
141
+ };
142
+ walk(runtimeRoot);
143
+ return copied;
144
+ }
88
145
  const matches = (pattern, line) => typeof pattern === "string" ? line.includes(pattern) : pattern.test(line);
89
146
  /** Every line, in any container of the instance, that matches a forbidden
90
147
  * pattern. `clean` when there are none — including when docker cannot be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/smoke.js CHANGED
@@ -19,7 +19,7 @@
19
19
  // rule); a product wanting typed commands bridges with toArgv in its own spec.
20
20
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
- import { dumpInstanceContainerLogs } from "./container-logs.js";
22
+ import { dumpInstanceContainerLogs, preserveEngineLogs } from "./container-logs.js";
23
23
  import { ensureRunnerOnNetwork, netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
24
24
  import { cleanupDaemon, requireLicenseFile, runCli, startDaemon, } from "./daemon.js";
25
25
  import { hashSlot } from "./harness-config.js";
@@ -391,6 +391,17 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
391
391
  catch (e) {
392
392
  journeyError = e;
393
393
  }
394
+ // Preserve the engine logs BEFORE anything tears the store down. norsk-ctl
395
+ // bind-mounts each media container's /var/log/norsk to the store, so the full
396
+ // structured debug.json is on disk here -- but `product remove`,
397
+ // `instance delete --purge`, cleanup and the root nuke below all destroy it.
398
+ // We pretty much always want the Norsk logs from a smoke run; when a CI log
399
+ // dir is set, copy every logs/ tree out first so the workflow can upload it.
400
+ const ciLogDir = process.env.NORSK_CI_LOG_DIR;
401
+ if (ciLogDir) {
402
+ const n = preserveEngineLogs(join(storeDir, "norsk-runtime"), join(ciLogDir, slug));
403
+ deps.log(`${slug}: preserved ${n} engine log tree(s) -> ${join(ciLogDir, slug)}`);
404
+ }
394
405
  if (handles.length)
395
406
  await deps.stopSources(handles).catch(() => { });
396
407
  // The daemon reaps its own control-plane containers on shutdown (daemon.ts,