ccqa-tools 1.37.0 → 1.38.0
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.
- package/README.md +3 -2
- package/dist/coverage/collector.cjs +62 -1
- package/dist/coverage/collector.d.cts +32 -1
- package/dist/coverage/collector.d.ts +32 -1
- package/dist/coverage/collector.js +59 -2
- package/dist/coverage/register.cjs +58 -10
- package/dist/coverage/register.js +58 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,8 +52,9 @@ application pays nothing at all.
|
|
|
52
52
|
| Variable | Meaning |
|
|
53
53
|
| --- | --- |
|
|
54
54
|
| `CCQA_COVERAGE` | `1` to enable. Any other value is a `<runId>.<specId>` and becomes the ambient spec for a process with no inbound request to read — a worker started per spec. |
|
|
55
|
-
| `CCQA_COVERAGE_ENDPOINT` | Where to push.
|
|
56
|
-
| `CCQA_COVERAGE_TOKEN` | Sent as a bearer token
|
|
55
|
+
| `CCQA_COVERAGE_ENDPOINT` | Where to push. Defaults to `http://127.0.0.1:4757`, the loopback sink a local `ccqa run --coverage` binds; a deployed application sets its own. |
|
|
56
|
+
| `CCQA_COVERAGE_TOKEN` | Sent as a bearer token. The hub's coverage inbox verifies it; the loopback sink a local run binds does not check it, as before. |
|
|
57
|
+
| `CCQA_COVERAGE_HEADER` | One extra `name:value` header sent with every push, for a load balancer that gates the endpoint on a header. |
|
|
57
58
|
| `CCQA_COVERAGE_ROOT` | Root that file ids are relative to. Defaults to `process.cwd()`. In a workspace, point it at a directory containing the sibling packages too, and give `ccqa` the same one through `coverage.projectRoot`. |
|
|
58
59
|
| `CCQA_COVERAGE_INCLUDE` | Comma-separated directories to instrument, relative to the root. Defaults to `src`. |
|
|
59
60
|
| `CCQA_COVERAGE_DEBUG` | `1` for diagnostics on stderr. |
|
|
@@ -26,6 +26,17 @@ function closeActorBucket(runtime, key) {
|
|
|
26
26
|
function armGate(runtime) {
|
|
27
27
|
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Sent as a bearer token. The hub's coverage inbox verifies it; the loopback
|
|
31
|
+
* sink a local run binds does not check it, as before — there it is carried
|
|
32
|
+
* for a relay in front of the sink.
|
|
33
|
+
*/
|
|
34
|
+
const ENV_TOKEN = "CCQA_COVERAGE_TOKEN";
|
|
35
|
+
/**
|
|
36
|
+
* One extra `name:value` header sent with every push, for a load balancer
|
|
37
|
+
* that gates the endpoint on a header.
|
|
38
|
+
*/
|
|
39
|
+
const ENV_HEADER = "CCQA_COVERAGE_HEADER";
|
|
29
40
|
//#endregion
|
|
30
41
|
//#region src/coverage/runtime-env.ts
|
|
31
42
|
/**
|
|
@@ -48,6 +59,42 @@ function debugLog(config, message) {
|
|
|
48
59
|
* that commutative, associative and idempotent, so the sink never has to know
|
|
49
60
|
* how many replicas there were.
|
|
50
61
|
*/
|
|
62
|
+
/** The loopback inbox a local `ccqa run --coverage` binds — its default too. */
|
|
63
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:4757";
|
|
64
|
+
/**
|
|
65
|
+
* Env → options, at register time. The endpoint defaults to the run's
|
|
66
|
+
* loopback inbox, so a local stack needs no endpoint configuration at all;
|
|
67
|
+
* only a deployed environment writes one.
|
|
68
|
+
*/
|
|
69
|
+
function collectorOptionsFromEnv(env, config) {
|
|
70
|
+
const endpoint = env["CCQA_COVERAGE_ENDPOINT"] || "http://127.0.0.1:4757";
|
|
71
|
+
if (config && !env["CCQA_COVERAGE_ENDPOINT"]) debugLog(config, `endpoint defaulted to ${DEFAULT_ENDPOINT}`);
|
|
72
|
+
return {
|
|
73
|
+
endpoint,
|
|
74
|
+
token: env[ENV_TOKEN],
|
|
75
|
+
header: parseHeader(env[ENV_HEADER])
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `name:value` for the one extra push header, split on the first colon so the
|
|
80
|
+
* value may contain more. Malformed input is warned about rather than silently
|
|
81
|
+
* dropped: the header exists to pass a gateway, and a push missing it fails
|
|
82
|
+
* looking exactly like an unreachable sink. The value may be a gateway secret,
|
|
83
|
+
* so the warning names the variable and never echoes its content.
|
|
84
|
+
*/
|
|
85
|
+
function parseHeader(raw) {
|
|
86
|
+
if (!raw) return void 0;
|
|
87
|
+
const colon = raw.indexOf(":");
|
|
88
|
+
const name = colon < 0 ? "" : raw.slice(0, colon).trim();
|
|
89
|
+
if (name === "") {
|
|
90
|
+
process.stderr.write(`[ccqa-tools] ignoring ${ENV_HEADER}: expected "name:value"\n`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
value: raw.slice(colon + 1).trim()
|
|
96
|
+
};
|
|
97
|
+
}
|
|
51
98
|
const DEFAULT_INTERVAL_MS = 1e3;
|
|
52
99
|
const DEFAULT_IDLE_TTL_MS = 12e4;
|
|
53
100
|
/**
|
|
@@ -113,7 +160,7 @@ function startCollector(options, config) {
|
|
|
113
160
|
state.droppedPushes++;
|
|
114
161
|
consecutiveFailures++;
|
|
115
162
|
if (config) debugLog(config, `push failed: ${String(error)}`);
|
|
116
|
-
if (consecutiveFailures
|
|
163
|
+
if (shouldWarnPushFailure(consecutiveFailures)) process.stderr.write(`[ccqa-tools] push to ${options.endpoint} failed ${consecutiveFailures} times in a row: ${String(error)}\n`);
|
|
117
164
|
}).finally(() => {
|
|
118
165
|
inFlight = false;
|
|
119
166
|
});
|
|
@@ -133,6 +180,15 @@ function startCollector(options, config) {
|
|
|
133
180
|
};
|
|
134
181
|
}
|
|
135
182
|
/**
|
|
183
|
+
* Which consecutive-failure counts warn: 1, 10, 100, then every 1000th.
|
|
184
|
+
* Backing off rather than a fixed every-10: the endpoint now has a default,
|
|
185
|
+
* so a deployed process whose endpoint was never configured fails every tick
|
|
186
|
+
* for its whole life — a permanent once-per-ten-seconds drone, not a burst.
|
|
187
|
+
*/
|
|
188
|
+
function shouldWarnPushFailure(consecutiveFailures) {
|
|
189
|
+
return consecutiveFailures === 1 || consecutiveFailures === 10 || consecutiveFailures === 100 || consecutiveFailures % 1e3 === 0;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
136
192
|
* `acked` is always a subset of `reached`, and neither ever shrinks, so equal
|
|
137
193
|
* sizes mean nothing new — skip the copy-then-filter and check that first.
|
|
138
194
|
*/
|
|
@@ -213,6 +269,7 @@ function dropQuiet(entries, sent, state, cutoff, now, close) {
|
|
|
213
269
|
async function post(options, payload) {
|
|
214
270
|
const headers = { "content-type": "application/json" };
|
|
215
271
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
272
|
+
if (options.header) headers[options.header.name] = options.header.value;
|
|
216
273
|
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
217
274
|
const response = await fetch(options.endpoint, {
|
|
218
275
|
method: "POST",
|
|
@@ -223,7 +280,11 @@ async function post(options, payload) {
|
|
|
223
280
|
if (!response.ok) throw new Error(`sink returned ${response.status}`);
|
|
224
281
|
}
|
|
225
282
|
//#endregion
|
|
283
|
+
exports.DEFAULT_ENDPOINT = DEFAULT_ENDPOINT;
|
|
284
|
+
exports.collectorOptionsFromEnv = collectorOptionsFromEnv;
|
|
226
285
|
exports.createCollectorState = createCollectorState;
|
|
227
286
|
exports.diff = diff;
|
|
228
287
|
exports.evict = evict;
|
|
288
|
+
exports.parseHeader = parseHeader;
|
|
289
|
+
exports.shouldWarnPushFailure = shouldWarnPushFailure;
|
|
229
290
|
exports.startCollector = startCollector;
|
|
@@ -117,10 +117,34 @@ interface CoverageConfig {
|
|
|
117
117
|
interface CollectorOptions {
|
|
118
118
|
endpoint: string;
|
|
119
119
|
token?: string | undefined;
|
|
120
|
+
/** One extra header on every push, for a gateway in front of the endpoint. */
|
|
121
|
+
header?: {
|
|
122
|
+
name: string;
|
|
123
|
+
value: string;
|
|
124
|
+
} | undefined;
|
|
120
125
|
intervalMs?: number;
|
|
121
126
|
/** Drops a spec's bucket this long after its last change and last flush. */
|
|
122
127
|
idleTtlMs?: number;
|
|
123
128
|
}
|
|
129
|
+
/** The loopback inbox a local `ccqa run --coverage` binds — its default too. */
|
|
130
|
+
declare const DEFAULT_ENDPOINT = "http://127.0.0.1:4757";
|
|
131
|
+
/**
|
|
132
|
+
* Env → options, at register time. The endpoint defaults to the run's
|
|
133
|
+
* loopback inbox, so a local stack needs no endpoint configuration at all;
|
|
134
|
+
* only a deployed environment writes one.
|
|
135
|
+
*/
|
|
136
|
+
declare function collectorOptionsFromEnv(env: NodeJS.ProcessEnv, config?: CoverageConfig): CollectorOptions;
|
|
137
|
+
/**
|
|
138
|
+
* `name:value` for the one extra push header, split on the first colon so the
|
|
139
|
+
* value may contain more. Malformed input is warned about rather than silently
|
|
140
|
+
* dropped: the header exists to pass a gateway, and a push missing it fails
|
|
141
|
+
* looking exactly like an unreachable sink. The value may be a gateway secret,
|
|
142
|
+
* so the warning names the variable and never echoes its content.
|
|
143
|
+
*/
|
|
144
|
+
declare function parseHeader(raw: string | undefined): {
|
|
145
|
+
name: string;
|
|
146
|
+
value: string;
|
|
147
|
+
} | undefined;
|
|
124
148
|
interface CoveragePush {
|
|
125
149
|
protocol: 1;
|
|
126
150
|
pid: number;
|
|
@@ -171,6 +195,13 @@ interface CollectorState {
|
|
|
171
195
|
}
|
|
172
196
|
declare function createCollectorState(): CollectorState;
|
|
173
197
|
declare function startCollector(options: CollectorOptions, config?: CoverageConfig): () => void;
|
|
198
|
+
/**
|
|
199
|
+
* Which consecutive-failure counts warn: 1, 10, 100, then every 1000th.
|
|
200
|
+
* Backing off rather than a fixed every-10: the endpoint now has a default,
|
|
201
|
+
* so a deployed process whose endpoint was never configured fails every tick
|
|
202
|
+
* for its whole life — a permanent once-per-ten-seconds drone, not a burst.
|
|
203
|
+
*/
|
|
204
|
+
declare function shouldWarnPushFailure(consecutiveFailures: number): boolean;
|
|
174
205
|
/** Exported for tests: the sink's HTTP round trip is the only real-I/O part. */
|
|
175
206
|
declare function diff(runtime: CoverageRuntime, state: CollectorState): CoveragePush | undefined;
|
|
176
207
|
/**
|
|
@@ -180,4 +211,4 @@ declare function diff(runtime: CoverageRuntime, state: CollectorState): Coverage
|
|
|
180
211
|
*/
|
|
181
212
|
declare function evict(runtime: CoverageRuntime, state: CollectorState, idleTtlMs: number): void;
|
|
182
213
|
//#endregion
|
|
183
|
-
export { CollectorOptions, CollectorState, CoveragePush, createCollectorState, diff, evict, startCollector };
|
|
214
|
+
export { CollectorOptions, CollectorState, CoveragePush, DEFAULT_ENDPOINT, collectorOptionsFromEnv, createCollectorState, diff, evict, parseHeader, shouldWarnPushFailure, startCollector };
|
|
@@ -117,10 +117,34 @@ interface CoverageConfig {
|
|
|
117
117
|
interface CollectorOptions {
|
|
118
118
|
endpoint: string;
|
|
119
119
|
token?: string | undefined;
|
|
120
|
+
/** One extra header on every push, for a gateway in front of the endpoint. */
|
|
121
|
+
header?: {
|
|
122
|
+
name: string;
|
|
123
|
+
value: string;
|
|
124
|
+
} | undefined;
|
|
120
125
|
intervalMs?: number;
|
|
121
126
|
/** Drops a spec's bucket this long after its last change and last flush. */
|
|
122
127
|
idleTtlMs?: number;
|
|
123
128
|
}
|
|
129
|
+
/** The loopback inbox a local `ccqa run --coverage` binds — its default too. */
|
|
130
|
+
declare const DEFAULT_ENDPOINT = "http://127.0.0.1:4757";
|
|
131
|
+
/**
|
|
132
|
+
* Env → options, at register time. The endpoint defaults to the run's
|
|
133
|
+
* loopback inbox, so a local stack needs no endpoint configuration at all;
|
|
134
|
+
* only a deployed environment writes one.
|
|
135
|
+
*/
|
|
136
|
+
declare function collectorOptionsFromEnv(env: NodeJS.ProcessEnv, config?: CoverageConfig): CollectorOptions;
|
|
137
|
+
/**
|
|
138
|
+
* `name:value` for the one extra push header, split on the first colon so the
|
|
139
|
+
* value may contain more. Malformed input is warned about rather than silently
|
|
140
|
+
* dropped: the header exists to pass a gateway, and a push missing it fails
|
|
141
|
+
* looking exactly like an unreachable sink. The value may be a gateway secret,
|
|
142
|
+
* so the warning names the variable and never echoes its content.
|
|
143
|
+
*/
|
|
144
|
+
declare function parseHeader(raw: string | undefined): {
|
|
145
|
+
name: string;
|
|
146
|
+
value: string;
|
|
147
|
+
} | undefined;
|
|
124
148
|
interface CoveragePush {
|
|
125
149
|
protocol: 1;
|
|
126
150
|
pid: number;
|
|
@@ -171,6 +195,13 @@ interface CollectorState {
|
|
|
171
195
|
}
|
|
172
196
|
declare function createCollectorState(): CollectorState;
|
|
173
197
|
declare function startCollector(options: CollectorOptions, config?: CoverageConfig): () => void;
|
|
198
|
+
/**
|
|
199
|
+
* Which consecutive-failure counts warn: 1, 10, 100, then every 1000th.
|
|
200
|
+
* Backing off rather than a fixed every-10: the endpoint now has a default,
|
|
201
|
+
* so a deployed process whose endpoint was never configured fails every tick
|
|
202
|
+
* for its whole life — a permanent once-per-ten-seconds drone, not a burst.
|
|
203
|
+
*/
|
|
204
|
+
declare function shouldWarnPushFailure(consecutiveFailures: number): boolean;
|
|
174
205
|
/** Exported for tests: the sink's HTTP round trip is the only real-I/O part. */
|
|
175
206
|
declare function diff(runtime: CoverageRuntime, state: CollectorState): CoveragePush | undefined;
|
|
176
207
|
/**
|
|
@@ -180,4 +211,4 @@ declare function diff(runtime: CoverageRuntime, state: CollectorState): Coverage
|
|
|
180
211
|
*/
|
|
181
212
|
declare function evict(runtime: CoverageRuntime, state: CollectorState, idleTtlMs: number): void;
|
|
182
213
|
//#endregion
|
|
183
|
-
export { CollectorOptions, CollectorState, CoveragePush, createCollectorState, diff, evict, startCollector };
|
|
214
|
+
export { CollectorOptions, CollectorState, CoveragePush, DEFAULT_ENDPOINT, collectorOptionsFromEnv, createCollectorState, diff, evict, parseHeader, shouldWarnPushFailure, startCollector };
|
|
@@ -25,6 +25,17 @@ function closeActorBucket(runtime, key) {
|
|
|
25
25
|
function armGate(runtime) {
|
|
26
26
|
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Sent as a bearer token. The hub's coverage inbox verifies it; the loopback
|
|
30
|
+
* sink a local run binds does not check it, as before — there it is carried
|
|
31
|
+
* for a relay in front of the sink.
|
|
32
|
+
*/
|
|
33
|
+
const ENV_TOKEN = "CCQA_COVERAGE_TOKEN";
|
|
34
|
+
/**
|
|
35
|
+
* One extra `name:value` header sent with every push, for a load balancer
|
|
36
|
+
* that gates the endpoint on a header.
|
|
37
|
+
*/
|
|
38
|
+
const ENV_HEADER = "CCQA_COVERAGE_HEADER";
|
|
28
39
|
//#endregion
|
|
29
40
|
//#region src/coverage/runtime-env.ts
|
|
30
41
|
/**
|
|
@@ -47,6 +58,42 @@ function debugLog(config, message) {
|
|
|
47
58
|
* that commutative, associative and idempotent, so the sink never has to know
|
|
48
59
|
* how many replicas there were.
|
|
49
60
|
*/
|
|
61
|
+
/** The loopback inbox a local `ccqa run --coverage` binds — its default too. */
|
|
62
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:4757";
|
|
63
|
+
/**
|
|
64
|
+
* Env → options, at register time. The endpoint defaults to the run's
|
|
65
|
+
* loopback inbox, so a local stack needs no endpoint configuration at all;
|
|
66
|
+
* only a deployed environment writes one.
|
|
67
|
+
*/
|
|
68
|
+
function collectorOptionsFromEnv(env, config) {
|
|
69
|
+
const endpoint = env["CCQA_COVERAGE_ENDPOINT"] || "http://127.0.0.1:4757";
|
|
70
|
+
if (config && !env["CCQA_COVERAGE_ENDPOINT"]) debugLog(config, `endpoint defaulted to ${DEFAULT_ENDPOINT}`);
|
|
71
|
+
return {
|
|
72
|
+
endpoint,
|
|
73
|
+
token: env[ENV_TOKEN],
|
|
74
|
+
header: parseHeader(env[ENV_HEADER])
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `name:value` for the one extra push header, split on the first colon so the
|
|
79
|
+
* value may contain more. Malformed input is warned about rather than silently
|
|
80
|
+
* dropped: the header exists to pass a gateway, and a push missing it fails
|
|
81
|
+
* looking exactly like an unreachable sink. The value may be a gateway secret,
|
|
82
|
+
* so the warning names the variable and never echoes its content.
|
|
83
|
+
*/
|
|
84
|
+
function parseHeader(raw) {
|
|
85
|
+
if (!raw) return void 0;
|
|
86
|
+
const colon = raw.indexOf(":");
|
|
87
|
+
const name = colon < 0 ? "" : raw.slice(0, colon).trim();
|
|
88
|
+
if (name === "") {
|
|
89
|
+
process.stderr.write(`[ccqa-tools] ignoring ${ENV_HEADER}: expected "name:value"\n`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
value: raw.slice(colon + 1).trim()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
50
97
|
const DEFAULT_INTERVAL_MS = 1e3;
|
|
51
98
|
const DEFAULT_IDLE_TTL_MS = 12e4;
|
|
52
99
|
/**
|
|
@@ -112,7 +159,7 @@ function startCollector(options, config) {
|
|
|
112
159
|
state.droppedPushes++;
|
|
113
160
|
consecutiveFailures++;
|
|
114
161
|
if (config) debugLog(config, `push failed: ${String(error)}`);
|
|
115
|
-
if (consecutiveFailures
|
|
162
|
+
if (shouldWarnPushFailure(consecutiveFailures)) process.stderr.write(`[ccqa-tools] push to ${options.endpoint} failed ${consecutiveFailures} times in a row: ${String(error)}\n`);
|
|
116
163
|
}).finally(() => {
|
|
117
164
|
inFlight = false;
|
|
118
165
|
});
|
|
@@ -132,6 +179,15 @@ function startCollector(options, config) {
|
|
|
132
179
|
};
|
|
133
180
|
}
|
|
134
181
|
/**
|
|
182
|
+
* Which consecutive-failure counts warn: 1, 10, 100, then every 1000th.
|
|
183
|
+
* Backing off rather than a fixed every-10: the endpoint now has a default,
|
|
184
|
+
* so a deployed process whose endpoint was never configured fails every tick
|
|
185
|
+
* for its whole life — a permanent once-per-ten-seconds drone, not a burst.
|
|
186
|
+
*/
|
|
187
|
+
function shouldWarnPushFailure(consecutiveFailures) {
|
|
188
|
+
return consecutiveFailures === 1 || consecutiveFailures === 10 || consecutiveFailures === 100 || consecutiveFailures % 1e3 === 0;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
135
191
|
* `acked` is always a subset of `reached`, and neither ever shrinks, so equal
|
|
136
192
|
* sizes mean nothing new — skip the copy-then-filter and check that first.
|
|
137
193
|
*/
|
|
@@ -212,6 +268,7 @@ function dropQuiet(entries, sent, state, cutoff, now, close) {
|
|
|
212
268
|
async function post(options, payload) {
|
|
213
269
|
const headers = { "content-type": "application/json" };
|
|
214
270
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
271
|
+
if (options.header) headers[options.header.name] = options.header.value;
|
|
215
272
|
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
216
273
|
const response = await fetch(options.endpoint, {
|
|
217
274
|
method: "POST",
|
|
@@ -222,4 +279,4 @@ async function post(options, payload) {
|
|
|
222
279
|
if (!response.ok) throw new Error(`sink returned ${response.status}`);
|
|
223
280
|
}
|
|
224
281
|
//#endregion
|
|
225
|
-
export { createCollectorState, diff, evict, startCollector };
|
|
282
|
+
export { DEFAULT_ENDPOINT, collectorOptionsFromEnv, createCollectorState, diff, evict, parseHeader, shouldWarnPushFailure, startCollector };
|
|
@@ -156,10 +156,17 @@ const BAGGAGE_KEY = "ccqa.coverage";
|
|
|
156
156
|
* inbound request to read, such as a worker started per spec.
|
|
157
157
|
*/
|
|
158
158
|
const ENV_NAME = "CCQA_COVERAGE";
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Sent as a bearer token. The hub's coverage inbox verifies it; the loopback
|
|
161
|
+
* sink a local run binds does not check it, as before — there it is carried
|
|
162
|
+
* for a relay in front of the sink.
|
|
163
|
+
*/
|
|
162
164
|
const ENV_TOKEN = "CCQA_COVERAGE_TOKEN";
|
|
165
|
+
/**
|
|
166
|
+
* One extra `name:value` header sent with every push, for a load balancer
|
|
167
|
+
* that gates the endpoint on a header.
|
|
168
|
+
*/
|
|
169
|
+
const ENV_HEADER = "CCQA_COVERAGE_HEADER";
|
|
163
170
|
const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
|
|
164
171
|
/**
|
|
165
172
|
* Accepts a carrier value only if it looks like an id we wrote.
|
|
@@ -236,6 +243,42 @@ function debugLog(config, message) {
|
|
|
236
243
|
* that commutative, associative and idempotent, so the sink never has to know
|
|
237
244
|
* how many replicas there were.
|
|
238
245
|
*/
|
|
246
|
+
/** The loopback inbox a local `ccqa run --coverage` binds — its default too. */
|
|
247
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:4757";
|
|
248
|
+
/**
|
|
249
|
+
* Env → options, at register time. The endpoint defaults to the run's
|
|
250
|
+
* loopback inbox, so a local stack needs no endpoint configuration at all;
|
|
251
|
+
* only a deployed environment writes one.
|
|
252
|
+
*/
|
|
253
|
+
function collectorOptionsFromEnv(env, config) {
|
|
254
|
+
const endpoint = env["CCQA_COVERAGE_ENDPOINT"] || "http://127.0.0.1:4757";
|
|
255
|
+
if (config && !env["CCQA_COVERAGE_ENDPOINT"]) debugLog(config, `endpoint defaulted to ${DEFAULT_ENDPOINT}`);
|
|
256
|
+
return {
|
|
257
|
+
endpoint,
|
|
258
|
+
token: env[ENV_TOKEN],
|
|
259
|
+
header: parseHeader(env[ENV_HEADER])
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* `name:value` for the one extra push header, split on the first colon so the
|
|
264
|
+
* value may contain more. Malformed input is warned about rather than silently
|
|
265
|
+
* dropped: the header exists to pass a gateway, and a push missing it fails
|
|
266
|
+
* looking exactly like an unreachable sink. The value may be a gateway secret,
|
|
267
|
+
* so the warning names the variable and never echoes its content.
|
|
268
|
+
*/
|
|
269
|
+
function parseHeader(raw) {
|
|
270
|
+
if (!raw) return void 0;
|
|
271
|
+
const colon = raw.indexOf(":");
|
|
272
|
+
const name = colon < 0 ? "" : raw.slice(0, colon).trim();
|
|
273
|
+
if (name === "") {
|
|
274
|
+
process.stderr.write(`[ccqa-tools] ignoring ${ENV_HEADER}: expected "name:value"\n`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
name,
|
|
279
|
+
value: raw.slice(colon + 1).trim()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
239
282
|
const DEFAULT_INTERVAL_MS = 1e3;
|
|
240
283
|
const DEFAULT_IDLE_TTL_MS = 12e4;
|
|
241
284
|
/**
|
|
@@ -301,7 +344,7 @@ function startCollector(options, config) {
|
|
|
301
344
|
state.droppedPushes++;
|
|
302
345
|
consecutiveFailures++;
|
|
303
346
|
if (config) debugLog(config, `push failed: ${String(error)}`);
|
|
304
|
-
if (consecutiveFailures
|
|
347
|
+
if (shouldWarnPushFailure(consecutiveFailures)) process.stderr.write(`[ccqa-tools] push to ${options.endpoint} failed ${consecutiveFailures} times in a row: ${String(error)}\n`);
|
|
305
348
|
}).finally(() => {
|
|
306
349
|
inFlight = false;
|
|
307
350
|
});
|
|
@@ -321,6 +364,15 @@ function startCollector(options, config) {
|
|
|
321
364
|
};
|
|
322
365
|
}
|
|
323
366
|
/**
|
|
367
|
+
* Which consecutive-failure counts warn: 1, 10, 100, then every 1000th.
|
|
368
|
+
* Backing off rather than a fixed every-10: the endpoint now has a default,
|
|
369
|
+
* so a deployed process whose endpoint was never configured fails every tick
|
|
370
|
+
* for its whole life — a permanent once-per-ten-seconds drone, not a burst.
|
|
371
|
+
*/
|
|
372
|
+
function shouldWarnPushFailure(consecutiveFailures) {
|
|
373
|
+
return consecutiveFailures === 1 || consecutiveFailures === 10 || consecutiveFailures === 100 || consecutiveFailures % 1e3 === 0;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
324
376
|
* `acked` is always a subset of `reached`, and neither ever shrinks, so equal
|
|
325
377
|
* sizes mean nothing new — skip the copy-then-filter and check that first.
|
|
326
378
|
*/
|
|
@@ -401,6 +453,7 @@ function dropQuiet(entries, sent, state, cutoff, now, close) {
|
|
|
401
453
|
async function post(options, payload) {
|
|
402
454
|
const headers = { "content-type": "application/json" };
|
|
403
455
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
456
|
+
if (options.header) headers[options.header.name] = options.header.value;
|
|
404
457
|
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
405
458
|
const response = await fetch(options.endpoint, {
|
|
406
459
|
method: "POST",
|
|
@@ -707,12 +760,7 @@ if (config.enabled) {
|
|
|
707
760
|
files
|
|
708
761
|
});
|
|
709
762
|
}
|
|
710
|
-
|
|
711
|
-
if (endpoint) startCollector({
|
|
712
|
-
endpoint,
|
|
713
|
-
token: process.env[ENV_TOKEN]
|
|
714
|
-
}, config);
|
|
715
|
-
else process.stderr.write(`[ccqa-tools] collection is enabled but ${ENV_ENDPOINT} is not set; results will be discarded\n`);
|
|
763
|
+
startCollector(collectorOptionsFromEnv(process.env, config), config);
|
|
716
764
|
debugLog(config, `armed in pid ${process.pid}`);
|
|
717
765
|
}
|
|
718
766
|
/**
|
|
@@ -131,10 +131,17 @@ const BAGGAGE_KEY = "ccqa.coverage";
|
|
|
131
131
|
* inbound request to read, such as a worker started per spec.
|
|
132
132
|
*/
|
|
133
133
|
const ENV_NAME = "CCQA_COVERAGE";
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Sent as a bearer token. The hub's coverage inbox verifies it; the loopback
|
|
136
|
+
* sink a local run binds does not check it, as before — there it is carried
|
|
137
|
+
* for a relay in front of the sink.
|
|
138
|
+
*/
|
|
137
139
|
const ENV_TOKEN = "CCQA_COVERAGE_TOKEN";
|
|
140
|
+
/**
|
|
141
|
+
* One extra `name:value` header sent with every push, for a load balancer
|
|
142
|
+
* that gates the endpoint on a header.
|
|
143
|
+
*/
|
|
144
|
+
const ENV_HEADER = "CCQA_COVERAGE_HEADER";
|
|
138
145
|
const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
|
|
139
146
|
/**
|
|
140
147
|
* Accepts a carrier value only if it looks like an id we wrote.
|
|
@@ -211,6 +218,42 @@ function debugLog(config, message) {
|
|
|
211
218
|
* that commutative, associative and idempotent, so the sink never has to know
|
|
212
219
|
* how many replicas there were.
|
|
213
220
|
*/
|
|
221
|
+
/** The loopback inbox a local `ccqa run --coverage` binds — its default too. */
|
|
222
|
+
const DEFAULT_ENDPOINT = "http://127.0.0.1:4757";
|
|
223
|
+
/**
|
|
224
|
+
* Env → options, at register time. The endpoint defaults to the run's
|
|
225
|
+
* loopback inbox, so a local stack needs no endpoint configuration at all;
|
|
226
|
+
* only a deployed environment writes one.
|
|
227
|
+
*/
|
|
228
|
+
function collectorOptionsFromEnv(env, config) {
|
|
229
|
+
const endpoint = env["CCQA_COVERAGE_ENDPOINT"] || "http://127.0.0.1:4757";
|
|
230
|
+
if (config && !env["CCQA_COVERAGE_ENDPOINT"]) debugLog(config, `endpoint defaulted to ${DEFAULT_ENDPOINT}`);
|
|
231
|
+
return {
|
|
232
|
+
endpoint,
|
|
233
|
+
token: env[ENV_TOKEN],
|
|
234
|
+
header: parseHeader(env[ENV_HEADER])
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `name:value` for the one extra push header, split on the first colon so the
|
|
239
|
+
* value may contain more. Malformed input is warned about rather than silently
|
|
240
|
+
* dropped: the header exists to pass a gateway, and a push missing it fails
|
|
241
|
+
* looking exactly like an unreachable sink. The value may be a gateway secret,
|
|
242
|
+
* so the warning names the variable and never echoes its content.
|
|
243
|
+
*/
|
|
244
|
+
function parseHeader(raw) {
|
|
245
|
+
if (!raw) return void 0;
|
|
246
|
+
const colon = raw.indexOf(":");
|
|
247
|
+
const name = colon < 0 ? "" : raw.slice(0, colon).trim();
|
|
248
|
+
if (name === "") {
|
|
249
|
+
process.stderr.write(`[ccqa-tools] ignoring ${ENV_HEADER}: expected "name:value"\n`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
name,
|
|
254
|
+
value: raw.slice(colon + 1).trim()
|
|
255
|
+
};
|
|
256
|
+
}
|
|
214
257
|
const DEFAULT_INTERVAL_MS = 1e3;
|
|
215
258
|
const DEFAULT_IDLE_TTL_MS = 12e4;
|
|
216
259
|
/**
|
|
@@ -276,7 +319,7 @@ function startCollector(options, config) {
|
|
|
276
319
|
state.droppedPushes++;
|
|
277
320
|
consecutiveFailures++;
|
|
278
321
|
if (config) debugLog(config, `push failed: ${String(error)}`);
|
|
279
|
-
if (consecutiveFailures
|
|
322
|
+
if (shouldWarnPushFailure(consecutiveFailures)) process.stderr.write(`[ccqa-tools] push to ${options.endpoint} failed ${consecutiveFailures} times in a row: ${String(error)}\n`);
|
|
280
323
|
}).finally(() => {
|
|
281
324
|
inFlight = false;
|
|
282
325
|
});
|
|
@@ -296,6 +339,15 @@ function startCollector(options, config) {
|
|
|
296
339
|
};
|
|
297
340
|
}
|
|
298
341
|
/**
|
|
342
|
+
* Which consecutive-failure counts warn: 1, 10, 100, then every 1000th.
|
|
343
|
+
* Backing off rather than a fixed every-10: the endpoint now has a default,
|
|
344
|
+
* so a deployed process whose endpoint was never configured fails every tick
|
|
345
|
+
* for its whole life — a permanent once-per-ten-seconds drone, not a burst.
|
|
346
|
+
*/
|
|
347
|
+
function shouldWarnPushFailure(consecutiveFailures) {
|
|
348
|
+
return consecutiveFailures === 1 || consecutiveFailures === 10 || consecutiveFailures === 100 || consecutiveFailures % 1e3 === 0;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
299
351
|
* `acked` is always a subset of `reached`, and neither ever shrinks, so equal
|
|
300
352
|
* sizes mean nothing new — skip the copy-then-filter and check that first.
|
|
301
353
|
*/
|
|
@@ -376,6 +428,7 @@ function dropQuiet(entries, sent, state, cutoff, now, close) {
|
|
|
376
428
|
async function post(options, payload) {
|
|
377
429
|
const headers = { "content-type": "application/json" };
|
|
378
430
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
431
|
+
if (options.header) headers[options.header.name] = options.header.value;
|
|
379
432
|
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
380
433
|
const response = await fetch(options.endpoint, {
|
|
381
434
|
method: "POST",
|
|
@@ -682,12 +735,7 @@ if (config.enabled) {
|
|
|
682
735
|
files
|
|
683
736
|
});
|
|
684
737
|
}
|
|
685
|
-
|
|
686
|
-
if (endpoint) startCollector({
|
|
687
|
-
endpoint,
|
|
688
|
-
token: process.env[ENV_TOKEN]
|
|
689
|
-
}, config);
|
|
690
|
-
else process.stderr.write(`[ccqa-tools] collection is enabled but ${ENV_ENDPOINT} is not set; results will be discarded\n`);
|
|
738
|
+
startCollector(collectorOptionsFromEnv(process.env, config), config);
|
|
691
739
|
debugLog(config, `armed in pid ${process.pid}`);
|
|
692
740
|
}
|
|
693
741
|
/**
|