@apifuse/provider-sdk 2.2.0-beta.55 → 2.2.0-beta.56
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/CHANGELOG.md +4 -0
- package/bin/apifuse-pack-types.ts +121 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/runtime/proxy-telemetry.d.ts +53 -2
- package/dist/runtime/proxy-telemetry.js +106 -65
- package/dist/runtime/request-telemetry.d.ts +70 -0
- package/dist/runtime/request-telemetry.js +228 -0
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +1 -0
- package/dist/server/serve-implementation.js +58 -20
- package/package.json +1 -1
- package/src/index.ts +13 -0
- package/src/runtime/proxy-telemetry.ts +160 -74
- package/src/runtime/request-telemetry.ts +376 -0
- package/src/server/index.ts +14 -0
- package/src/server/serve-implementation.ts +93 -21
package/CHANGELOG.md
CHANGED
|
@@ -292,6 +292,115 @@ const NEGATIVE_CONTROLS = [
|
|
|
292
292
|
"",
|
|
293
293
|
].join("\n"),
|
|
294
294
|
},
|
|
295
|
+
{
|
|
296
|
+
filename: "negative-control-telemetry-header-vendor.ts",
|
|
297
|
+
expectedCode: "TS2322",
|
|
298
|
+
description: "gateway telemetry headers reject an unbranded vendor string",
|
|
299
|
+
source: [
|
|
300
|
+
'import type { TelemetryContributor } from "@apifuse/provider-sdk";',
|
|
301
|
+
"",
|
|
302
|
+
"const bad: TelemetryContributor<{}, { vendor: string }> = {",
|
|
303
|
+
'\tkey: "resolver",',
|
|
304
|
+
"\ttoLogPayload: () => ({}),",
|
|
305
|
+
'\ttoHeaderPayload: () => ({ vendor: "free-text" }),',
|
|
306
|
+
"};",
|
|
307
|
+
"",
|
|
308
|
+
].join("\n"),
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
filename: "negative-control-telemetry-header-host.ts",
|
|
312
|
+
expectedCode: "TS2322",
|
|
313
|
+
description: "gateway telemetry headers reject an unbranded host string",
|
|
314
|
+
source: [
|
|
315
|
+
'import type { TelemetryContributor } from "@apifuse/provider-sdk";',
|
|
316
|
+
"",
|
|
317
|
+
"const bad: TelemetryContributor<{}, { host: string }> = {",
|
|
318
|
+
'\tkey: "http",',
|
|
319
|
+
"\ttoLogPayload: () => ({}),",
|
|
320
|
+
'\ttoHeaderPayload: () => ({ host: "x.example" }),',
|
|
321
|
+
"};",
|
|
322
|
+
"",
|
|
323
|
+
].join("\n"),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
filename: "negative-control-telemetry-tenant-vendor.ts",
|
|
327
|
+
expectedCode: "TS2322",
|
|
328
|
+
description: "tenant-neutral projections reject vendor identities",
|
|
329
|
+
source: [
|
|
330
|
+
'import type { TenantNeutral } from "@apifuse/provider-sdk";',
|
|
331
|
+
"",
|
|
332
|
+
'export const bad: TenantNeutral<{ vendorUsed: "smartproxy" }> = { vendorUsed: "smartproxy" };',
|
|
333
|
+
"",
|
|
334
|
+
].join("\n"),
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
filename: "negative-control-telemetry-header-any.ts",
|
|
338
|
+
expectedCode: "TS2322",
|
|
339
|
+
description: "gateway telemetry projections reject any-valued properties",
|
|
340
|
+
source: [
|
|
341
|
+
'import type { GatewayIngestible } from "@apifuse/provider-sdk";',
|
|
342
|
+
"",
|
|
343
|
+
'export const bad: GatewayIngestible<{ x: any }> = { x: "free-text" };',
|
|
344
|
+
"",
|
|
345
|
+
].join("\n"),
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
filename: "negative-control-telemetry-header-union-array.ts",
|
|
349
|
+
expectedCode: "TS2322",
|
|
350
|
+
description: "gateway telemetry projections reject arrays mixing numbers and strings",
|
|
351
|
+
source: [
|
|
352
|
+
'import type { TelemetryContributor } from "@apifuse/provider-sdk";',
|
|
353
|
+
"",
|
|
354
|
+
"const bad: TelemetryContributor<{}, { values: (number | string)[] }> = {",
|
|
355
|
+
'\tkey: "resolver",',
|
|
356
|
+
"\ttoLogPayload: () => ({}),",
|
|
357
|
+
'\ttoHeaderPayload: () => ({ values: [1, "free prose"] }),',
|
|
358
|
+
"};",
|
|
359
|
+
"",
|
|
360
|
+
].join("\n"),
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
filename: "negative-control-telemetry-opaque-identity.ts",
|
|
364
|
+
expectedCode: "TS2322",
|
|
365
|
+
description: "tenant opaque array brands are not accepted outside the cache keys property",
|
|
366
|
+
source: [
|
|
367
|
+
'import type { TenantNeutral } from "@apifuse/provider-sdk";',
|
|
368
|
+
"",
|
|
369
|
+
"type LocalOpaque = string[] & { readonly __tenantOpaqueCacheKeys: true };",
|
|
370
|
+
"declare const opaque: LocalOpaque;",
|
|
371
|
+
"export const bad: TenantNeutral<{ identity: LocalOpaque }> = { identity: opaque };",
|
|
372
|
+
"",
|
|
373
|
+
].join("\n"),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
filename: "negative-control-telemetry-meta-vendor.ts",
|
|
377
|
+
expectedCode: "TS2322",
|
|
378
|
+
description: "success metadata rejects tenant-visible vendor identity keys",
|
|
379
|
+
source: [
|
|
380
|
+
'import { closedEnum, type ClosedEnum, type TenantNeutral } from "@apifuse/provider-sdk";',
|
|
381
|
+
"",
|
|
382
|
+
'type Meta = { cached: boolean; detail: { vendorUsed: ClosedEnum<"smartproxy"> } };',
|
|
383
|
+
'export const bad: TenantNeutral<Meta> = { cached: false, detail: { vendorUsed: closedEnum("smartproxy") } };',
|
|
384
|
+
"",
|
|
385
|
+
].join("\n"),
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
filename: "positive-control-proxy-telemetry-contributor.ts",
|
|
389
|
+
expectedCode: "",
|
|
390
|
+
description: "proxy telemetry contributor satisfies the public contributor contract",
|
|
391
|
+
source: [
|
|
392
|
+
'import { closedEnum, type ClosedEnum, type GatewayIngestible, type ProxyTelemetryHeaderPayload, type ProxyTelemetryLogPayload, type TelemetryContributor } from "@apifuse/provider-sdk";',
|
|
393
|
+
"",
|
|
394
|
+
"export const proxy: TelemetryContributor<ProxyTelemetryLogPayload, ProxyTelemetryHeaderPayload> = {",
|
|
395
|
+
'\tkey: "proxy",',
|
|
396
|
+
'\ttoLogPayload: () => ({ kind: "unresolved", vendors: [] }),',
|
|
397
|
+
'\ttoHeaderPayload: () => ({ kind: closedEnum("unresolved"), vendors: [] }),',
|
|
398
|
+
"};",
|
|
399
|
+
'type Values = { values: ClosedEnum<"a" | "b">[] };',
|
|
400
|
+
'export const values: GatewayIngestible<Values> = { values: [closedEnum("a"), closedEnum("b")] };',
|
|
401
|
+
"",
|
|
402
|
+
].join("\n"),
|
|
403
|
+
},
|
|
295
404
|
] as const;
|
|
296
405
|
|
|
297
406
|
const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
|
|
@@ -513,6 +622,15 @@ function assertNegativeControlFails(consumerDir: string): void {
|
|
|
513
622
|
],
|
|
514
623
|
{ cwd: consumerDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
515
624
|
);
|
|
625
|
+
if (negativeControl.expectedCode === "") {
|
|
626
|
+
if (result.status !== 0) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Positive control "${negativeControl.description}" failed to compile:\n${result.stdout}\n${result.stderr}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
console.log(`Positive control accepted: ${negativeControl.description}`);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
516
634
|
if (result.status === 0) {
|
|
517
635
|
throw new Error(
|
|
518
636
|
'Negative control "' +
|
|
@@ -528,6 +646,9 @@ function assertNegativeControlFails(consumerDir: string): void {
|
|
|
528
646
|
`Negative control "${negativeControl.description}" (${negativeControl.filename}) failed for an unexpected reason (wanted ${negativeControl.expectedCode}):\n${output}`,
|
|
529
647
|
);
|
|
530
648
|
}
|
|
649
|
+
console.log(
|
|
650
|
+
`Negative control rejected (${negativeControl.expectedCode}): ${negativeControl.description}`,
|
|
651
|
+
);
|
|
531
652
|
}
|
|
532
653
|
}
|
|
533
654
|
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,8 @@ export { generateInsights } from "./runtime/insights.js";
|
|
|
32
32
|
export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
33
33
|
export type { PrevalidateResult } from "./runtime/prevalidate.js";
|
|
34
34
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
35
|
-
export type { ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "./runtime/proxy-telemetry.js";
|
|
35
|
+
export type { ProxyTelemetryHeaderPayload, ProxyHash, ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "./runtime/proxy-telemetry.js";
|
|
36
|
+
export { RequestTelemetry, closedEnum, type ClosedEnum, type GatewayIngestible, type RequestTelemetryLogPayload, type SpanIndex, type TelemetryContributor, type TelemetryKey, type TenantNeutral, } from "./runtime/request-telemetry.js";
|
|
36
37
|
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./runtime/resolver-config.js";
|
|
37
38
|
export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
|
|
38
39
|
export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdle
|
|
|
26
26
|
export { generateInsights } from "./runtime/insights.js";
|
|
27
27
|
export { wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
28
28
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
29
|
+
export { RequestTelemetry, closedEnum, } from "./runtime/request-telemetry.js";
|
|
29
30
|
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./runtime/resolver-config.js";
|
|
30
31
|
export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
|
|
31
32
|
export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ProxyAttemptTelemetryEvent, ProxyCacheStatus, ProxyProtocol, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyUserAgentSource, ProxyVendorFailoverTelemetryEvent, ProxyVendorName, SmartproxyAllocatorBodyClass } from "../config/loader.js";
|
|
2
|
-
|
|
2
|
+
import { type ClosedEnum, type TelemetryContributor } from "./request-telemetry.js";
|
|
3
|
+
export { PROVIDER_TELEMETRY_HEADER } from "./request-telemetry.js";
|
|
4
|
+
declare const PROXY_HASH: unique symbol;
|
|
5
|
+
/** Bounded hexadecimal host hash used in proxy attempt samples. */
|
|
6
|
+
export type ProxyHash = ClosedEnum<string> & {
|
|
7
|
+
readonly [PROXY_HASH]: true;
|
|
8
|
+
};
|
|
3
9
|
export type ProxyTelemetryResolvedPayload = {
|
|
4
10
|
kind: "resolved";
|
|
5
11
|
provider: ProxyVendorName;
|
|
@@ -77,11 +83,56 @@ export type ProxyTelemetryUnresolvedPayload = {
|
|
|
77
83
|
}[];
|
|
78
84
|
};
|
|
79
85
|
export type ProxyTelemetryLogPayload = ProxyTelemetryResolvedPayload | ProxyTelemetryUnresolvedPayload;
|
|
80
|
-
|
|
86
|
+
/** Concrete gateway-safe projection of the unchanged proxy sibling. */
|
|
87
|
+
export type ProxyTelemetryHeaderPayload = {
|
|
88
|
+
kind: ClosedEnum<"resolved" | "unresolved">;
|
|
89
|
+
provider?: ClosedEnum<ProxyVendorName>;
|
|
90
|
+
userAgentSource?: ClosedEnum<ProxyUserAgentSource>;
|
|
91
|
+
protocol?: ClosedEnum<ProxyProtocol>;
|
|
92
|
+
cacheStatus?: ClosedEnum<ProxyCacheStatus>;
|
|
93
|
+
cacheHit?: boolean;
|
|
94
|
+
resolutionMs?: number;
|
|
95
|
+
allocatorMs?: number;
|
|
96
|
+
allocatorStatus?: number;
|
|
97
|
+
allocatorBodyClass?: ClosedEnum<SmartproxyAllocatorBodyClass>;
|
|
98
|
+
allocatorAttempts?: number;
|
|
99
|
+
lockWaitMs?: number;
|
|
100
|
+
redisReadMs?: number;
|
|
101
|
+
redisWriteMs?: number;
|
|
102
|
+
poolAgeMs?: number;
|
|
103
|
+
poolExpiresInMs?: number;
|
|
104
|
+
attempts?: number;
|
|
105
|
+
refreshes?: number;
|
|
106
|
+
attemptSamples?: {
|
|
107
|
+
n: number;
|
|
108
|
+
a: number;
|
|
109
|
+
i?: number;
|
|
110
|
+
h?: ProxyHash;
|
|
111
|
+
o: ClosedEnum<ProxyAttemptTelemetryEvent["outcome"]>;
|
|
112
|
+
c?: ClosedEnum<string>;
|
|
113
|
+
s?: number;
|
|
114
|
+
d?: number;
|
|
115
|
+
}[];
|
|
116
|
+
vendors?: ClosedEnum<ProxyVendorName>[];
|
|
117
|
+
failovers?: {
|
|
118
|
+
v: ClosedEnum<ProxyVendorName>;
|
|
119
|
+
nx?: ClosedEnum<ProxyVendorName>;
|
|
120
|
+
p: ClosedEnum<ProxyVendorFailoverTelemetryEvent["phase"]>;
|
|
121
|
+
r: ClosedEnum<ProxyVendorFailoverTelemetryEvent["reason"]>;
|
|
122
|
+
a?: number;
|
|
123
|
+
}[];
|
|
124
|
+
};
|
|
125
|
+
export declare class ProxyTelemetryCollector implements ProxyTelemetrySink, TelemetryContributor<ProxyTelemetryLogPayload, ProxyTelemetryHeaderPayload> {
|
|
81
126
|
#private;
|
|
127
|
+
readonly key: "proxy";
|
|
82
128
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
|
|
129
|
+
/** Number of raw resolution events retained for bounded-history verification. */
|
|
130
|
+
get retainedResolutionEventCount(): number;
|
|
131
|
+
/** Total resolutions included in the incremental aggregate. */
|
|
132
|
+
get resolutionEventCount(): number;
|
|
83
133
|
recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
84
134
|
recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void;
|
|
85
135
|
toLogPayload(): ProxyTelemetryLogPayload | undefined;
|
|
136
|
+
toHeaderPayload(log: ProxyTelemetryLogPayload): ProxyTelemetryHeaderPayload;
|
|
86
137
|
toHeaderValue(): string | undefined;
|
|
87
138
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { closedEnum, RequestTelemetry, } from "./request-telemetry.js";
|
|
2
|
+
import { createTraceContext } from "./trace.js";
|
|
3
|
+
export { PROVIDER_TELEMETRY_HEADER } from "./request-telemetry.js";
|
|
3
4
|
const MAX_PROXY_ATTEMPT_SAMPLES = 24;
|
|
4
5
|
const MAX_PROXY_FAILOVER_SAMPLES = 12;
|
|
6
|
+
const MAX_RETAINED_PROXY_RESOLUTIONS = 64;
|
|
7
|
+
const PROXY_HEADER_TOKEN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
|
|
8
|
+
const PROXY_HEADER_HASH = /^[0-9a-f]{1,16}$/;
|
|
5
9
|
const CACHE_STATUS_SEVERITY = {
|
|
6
10
|
disabled: 0,
|
|
7
11
|
memory_hit: 1,
|
|
@@ -23,17 +27,39 @@ function maxOptional(left, right) {
|
|
|
23
27
|
function worseStatus(left, right) {
|
|
24
28
|
return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
|
|
25
29
|
}
|
|
26
|
-
function
|
|
27
|
-
return
|
|
30
|
+
function aggregateResolution(acc, event) {
|
|
31
|
+
return {
|
|
32
|
+
provider: event.provider,
|
|
33
|
+
userAgentSource: event.userAgentSource ?? acc.userAgentSource,
|
|
34
|
+
cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
|
|
35
|
+
cacheHit: acc.cacheHit && event.cacheHit,
|
|
36
|
+
resolutionMs: acc.resolutionMs + event.resolutionMs,
|
|
37
|
+
allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
|
|
38
|
+
allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
|
|
39
|
+
allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
|
|
40
|
+
allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
|
|
41
|
+
lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
|
|
42
|
+
redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
|
|
43
|
+
redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
|
|
44
|
+
poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
|
|
45
|
+
poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
|
|
46
|
+
attempts: acc.attempts + event.attempts,
|
|
47
|
+
refreshes: sumOptional(acc.refreshes, event.refreshes),
|
|
48
|
+
};
|
|
28
49
|
}
|
|
29
50
|
export class ProxyTelemetryCollector {
|
|
51
|
+
key = "proxy";
|
|
30
52
|
#events = [];
|
|
53
|
+
#resolutionCount = 0;
|
|
54
|
+
#aggregateResolution;
|
|
55
|
+
#lastSuccessfulResolution;
|
|
56
|
+
#vendors = [];
|
|
31
57
|
#attempts = [];
|
|
32
58
|
#failovers = [];
|
|
33
59
|
#unresolvedVendors = [];
|
|
34
60
|
recordProxyResolution(event) {
|
|
35
61
|
const outcome = event.outcome === "error" ? "error" : "ok";
|
|
36
|
-
|
|
62
|
+
const normalized = {
|
|
37
63
|
provider: event.provider,
|
|
38
64
|
outcome,
|
|
39
65
|
...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
|
|
@@ -58,11 +84,29 @@ export class ProxyTelemetryCollector {
|
|
|
58
84
|
: Math.max(0, Math.floor(event.poolExpiresInMs)),
|
|
59
85
|
attempts: Math.max(1, Math.floor(event.attempts || 1)),
|
|
60
86
|
refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
|
|
61
|
-
}
|
|
87
|
+
};
|
|
88
|
+
this.#resolutionCount += 1;
|
|
89
|
+
if (this.#events.length < MAX_RETAINED_PROXY_RESOLUTIONS)
|
|
90
|
+
this.#events.push(normalized);
|
|
91
|
+
this.#aggregateResolution = this.#aggregateResolution
|
|
92
|
+
? aggregateResolution(this.#aggregateResolution, normalized)
|
|
93
|
+
: normalized;
|
|
94
|
+
if (outcome === "ok")
|
|
95
|
+
this.#lastSuccessfulResolution = normalized;
|
|
96
|
+
if (!this.#vendors.includes(event.provider))
|
|
97
|
+
this.#vendors.push(event.provider);
|
|
62
98
|
if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
|
|
63
99
|
this.#unresolvedVendors.push(event.provider);
|
|
64
100
|
}
|
|
65
101
|
}
|
|
102
|
+
/** Number of raw resolution events retained for bounded-history verification. */
|
|
103
|
+
get retainedResolutionEventCount() {
|
|
104
|
+
return this.#events.length;
|
|
105
|
+
}
|
|
106
|
+
/** Total resolutions included in the incremental aggregate. */
|
|
107
|
+
get resolutionEventCount() {
|
|
108
|
+
return this.#resolutionCount;
|
|
109
|
+
}
|
|
66
110
|
recordProxyVendorFailover(event) {
|
|
67
111
|
if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
|
|
68
112
|
return;
|
|
@@ -96,8 +140,7 @@ export class ProxyTelemetryCollector {
|
|
|
96
140
|
});
|
|
97
141
|
}
|
|
98
142
|
toLogPayload() {
|
|
99
|
-
const
|
|
100
|
-
const okEvents = this.#events.filter((event) => event.outcome !== "error");
|
|
143
|
+
const aggregate = this.#aggregateResolution;
|
|
101
144
|
const attemptSamples = this.#attempts.map((attempt, index) => ({
|
|
102
145
|
n: index + 1,
|
|
103
146
|
a: attempt.attempt,
|
|
@@ -115,31 +158,9 @@ export class ProxyTelemetryCollector {
|
|
|
115
158
|
r: failover.reason,
|
|
116
159
|
...(failover.attempt === undefined ? {} : { a: failover.attempt }),
|
|
117
160
|
}));
|
|
118
|
-
if (
|
|
119
|
-
if (!
|
|
161
|
+
if (!this.#lastSuccessfulResolution) {
|
|
162
|
+
if (!aggregate && failovers.length === 0)
|
|
120
163
|
return undefined;
|
|
121
|
-
const failureEvents = this.#events.filter((event) => event.outcome === "error");
|
|
122
|
-
const [firstFailure, ...remainingFailures] = failureEvents;
|
|
123
|
-
const aggregate = firstFailure
|
|
124
|
-
? remainingFailures.reduce((acc, event) => ({
|
|
125
|
-
provider: event.provider,
|
|
126
|
-
outcome: "error",
|
|
127
|
-
cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
|
|
128
|
-
cacheHit: acc.cacheHit && event.cacheHit,
|
|
129
|
-
resolutionMs: acc.resolutionMs + event.resolutionMs,
|
|
130
|
-
allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
|
|
131
|
-
allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
|
|
132
|
-
allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
|
|
133
|
-
allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
|
|
134
|
-
lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
|
|
135
|
-
redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
|
|
136
|
-
redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
|
|
137
|
-
poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
|
|
138
|
-
poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
|
|
139
|
-
attempts: acc.attempts + event.attempts,
|
|
140
|
-
refreshes: sumOptional(acc.refreshes, event.refreshes),
|
|
141
|
-
}), firstFailure)
|
|
142
|
-
: undefined;
|
|
143
164
|
return {
|
|
144
165
|
kind: "unresolved",
|
|
145
166
|
vendors: [...this.#unresolvedVendors],
|
|
@@ -180,30 +201,9 @@ export class ProxyTelemetryCollector {
|
|
|
180
201
|
};
|
|
181
202
|
}
|
|
182
203
|
// The serving vendor/protocol is the last successful resolution.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (!vendors.includes(event.provider))
|
|
187
|
-
vendors.push(event.provider);
|
|
188
|
-
}
|
|
189
|
-
const aggregate = rest.reduce((acc, event) => ({
|
|
190
|
-
provider: event.provider,
|
|
191
|
-
userAgentSource: event.userAgentSource ?? acc.userAgentSource,
|
|
192
|
-
cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
|
|
193
|
-
cacheHit: acc.cacheHit && event.cacheHit,
|
|
194
|
-
resolutionMs: acc.resolutionMs + event.resolutionMs,
|
|
195
|
-
allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
|
|
196
|
-
allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
|
|
197
|
-
allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
|
|
198
|
-
allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
|
|
199
|
-
lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
|
|
200
|
-
redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
|
|
201
|
-
redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
|
|
202
|
-
poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
|
|
203
|
-
poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
|
|
204
|
-
attempts: acc.attempts + event.attempts,
|
|
205
|
-
refreshes: sumOptional(acc.refreshes, event.refreshes),
|
|
206
|
-
}), first);
|
|
204
|
+
if (!aggregate)
|
|
205
|
+
return undefined;
|
|
206
|
+
const serving = this.#lastSuccessfulResolution;
|
|
207
207
|
return {
|
|
208
208
|
kind: "resolved",
|
|
209
209
|
provider: serving.provider,
|
|
@@ -232,18 +232,59 @@ export class ProxyTelemetryCollector {
|
|
|
232
232
|
attempts: aggregate.attempts,
|
|
233
233
|
...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
|
|
234
234
|
...(attemptSamples.length > 0 ? { attemptSamples } : {}),
|
|
235
|
-
...(vendors.length > 1 ? { vendors } : {}),
|
|
235
|
+
...(this.#vendors.length > 1 ? { vendors: [...this.#vendors] } : {}),
|
|
236
236
|
...(failovers.length > 0 ? { failovers } : {}),
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
|
+
toHeaderPayload(log) {
|
|
240
|
+
const attemptSamples = log.attemptSamples?.map((sample) => ({
|
|
241
|
+
n: sample.n,
|
|
242
|
+
a: sample.a,
|
|
243
|
+
...(sample.i === undefined ? {} : { i: sample.i }),
|
|
244
|
+
...(sample.h && PROXY_HEADER_HASH.test(sample.h)
|
|
245
|
+
? { h: closedEnum(sample.h) }
|
|
246
|
+
: {}),
|
|
247
|
+
o: closedEnum(sample.o),
|
|
248
|
+
...(sample.c && PROXY_HEADER_TOKEN.test(sample.c) ? { c: closedEnum(sample.c) } : {}),
|
|
249
|
+
...(sample.s === undefined ? {} : { s: sample.s }),
|
|
250
|
+
...(sample.d === undefined ? {} : { d: sample.d }),
|
|
251
|
+
}));
|
|
252
|
+
const failovers = log.failovers?.map((failover) => ({
|
|
253
|
+
...failover,
|
|
254
|
+
v: closedEnum(failover.v),
|
|
255
|
+
...(failover.nx ? { nx: closedEnum(failover.nx) } : {}),
|
|
256
|
+
p: closedEnum(failover.p),
|
|
257
|
+
r: closedEnum(failover.r),
|
|
258
|
+
}));
|
|
259
|
+
if (log.kind === "resolved") {
|
|
260
|
+
return {
|
|
261
|
+
...log,
|
|
262
|
+
kind: closedEnum(log.kind),
|
|
263
|
+
provider: closedEnum(log.provider),
|
|
264
|
+
...(log.userAgentSource ? { userAgentSource: closedEnum(log.userAgentSource) } : {}),
|
|
265
|
+
...(log.protocol ? { protocol: closedEnum(log.protocol) } : {}),
|
|
266
|
+
cacheStatus: closedEnum(log.cacheStatus),
|
|
267
|
+
...(log.allocatorBodyClass
|
|
268
|
+
? { allocatorBodyClass: closedEnum(log.allocatorBodyClass) }
|
|
269
|
+
: {}),
|
|
270
|
+
...(attemptSamples ? { attemptSamples } : {}),
|
|
271
|
+
...(log.vendors ? { vendors: log.vendors.map((vendor) => closedEnum(vendor)) } : {}),
|
|
272
|
+
...(failovers ? { failovers } : {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
...log,
|
|
277
|
+
kind: closedEnum(log.kind),
|
|
278
|
+
vendors: log.vendors.map((vendor) => closedEnum(vendor)),
|
|
279
|
+
...(log.cacheStatus ? { cacheStatus: closedEnum(log.cacheStatus) } : {}),
|
|
280
|
+
...(log.allocatorBodyClass ? { allocatorBodyClass: closedEnum(log.allocatorBodyClass) } : {}),
|
|
281
|
+
...(failovers ? { failovers } : {}),
|
|
282
|
+
...(attemptSamples ? { attemptSamples } : {}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
239
285
|
toHeaderValue() {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const payload = { v: 1, proxy };
|
|
244
|
-
const encoded = encodeBase64Url(JSON.stringify(payload));
|
|
245
|
-
if (encoded.length > MAX_HEADER_BYTES)
|
|
246
|
-
return undefined;
|
|
247
|
-
return encoded;
|
|
286
|
+
const telemetry = new RequestTelemetry(createTraceContext());
|
|
287
|
+
telemetry.register(this);
|
|
288
|
+
return telemetry.toHeaderValue();
|
|
248
289
|
}
|
|
249
290
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ProxyTelemetrySink } from "../config/loader.js";
|
|
2
|
+
import type { ProxyTelemetryLogPayload } from "./proxy-telemetry.js";
|
|
3
|
+
import type { Span, TraceContext } from "./trace.js";
|
|
4
|
+
export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
|
|
5
|
+
export type TelemetryKey = "proxy" | "resolver" | "http" | "stealth" | "native" | "browser" | "ocr" | "stt" | "cache" | "state" | "events";
|
|
6
|
+
declare const CLOSED_ENUM: unique symbol;
|
|
7
|
+
/** A string from an SDK-declared, closed observability taxonomy. */
|
|
8
|
+
export type ClosedEnum<T extends string> = T & {
|
|
9
|
+
readonly [CLOSED_ENUM]: true;
|
|
10
|
+
};
|
|
11
|
+
/** Brands a value from an SDK-declared closed string union without changing it. */
|
|
12
|
+
export declare function closedEnum<T extends string>(value: T): ClosedEnum<T>;
|
|
13
|
+
/** Explicit exemption for the existing implementation-shaped cache key list. */
|
|
14
|
+
export type TenantOpaqueKeys = string[] & {
|
|
15
|
+
readonly __tenantOpaqueCacheKeys: true;
|
|
16
|
+
};
|
|
17
|
+
/** Names the deliberate tenant cache-key exemption without changing its wire value. */
|
|
18
|
+
export declare function tenantOpaqueKeys(value: string[]): TenantOpaqueKeys;
|
|
19
|
+
/**
|
|
20
|
+
* Compile-time projection for the gateway ingestion contract. Plain strings,
|
|
21
|
+
* URLs, hostnames, free text, unknown values, and escape-hatch properties reduce to `never`.
|
|
22
|
+
*/
|
|
23
|
+
export type GatewayIngestible<T> = 0 extends 1 & T ? never : T extends object ? keyof T extends never ? never : {
|
|
24
|
+
[K in keyof T]: 0 extends 1 & T[K] ? never : unknown extends T[K] ? never : [NonNullable<T[K]>] extends [number | boolean | ClosedEnum<string>] ? T[K] : [NonNullable<T[K]>] extends [readonly (infer U)[]] ? 0 extends 1 & U ? never : unknown extends U ? never : [U] extends [
|
|
25
|
+
number | boolean | ClosedEnum<string> | GatewayIngestible<U>
|
|
26
|
+
] ? T[K] : never : [NonNullable<T[K]>] extends [object] ? [NonNullable<T[K]>] extends [GatewayIngestible<NonNullable<T[K]>>] ? T[K] : never : never;
|
|
27
|
+
} : never;
|
|
28
|
+
/** Compile-time projection for tenant-visible, identity-neutral metadata. */
|
|
29
|
+
export type TenantNeutral<T> = 0 extends 1 & T ? never : T extends object ? {
|
|
30
|
+
[K in keyof T]: K extends `vendor${string}` | "provider" | "engine" | "model" | `${string}Host` ? never : 0 extends 1 & T[K] ? never : unknown extends T[K] ? never : [NonNullable<T[K]>] extends [
|
|
31
|
+
string[] & {
|
|
32
|
+
readonly __tenantOpaqueCacheKeys: true;
|
|
33
|
+
}
|
|
34
|
+
] ? K extends "keys" ? T[K] : never : [NonNullable<T[K]>] extends [number | boolean | ClosedEnum<string>] ? T[K] : [NonNullable<T[K]>] extends [object] ? [NonNullable<T[K]>] extends [TenantNeutral<NonNullable<T[K]>>] ? T[K] : never : never;
|
|
35
|
+
} : never;
|
|
36
|
+
export interface SpanIndex {
|
|
37
|
+
readonly spans: readonly Span[];
|
|
38
|
+
readonly byName: ReadonlyMap<string, readonly Span[]>;
|
|
39
|
+
count(name: string): number;
|
|
40
|
+
durationMs(name: string): number;
|
|
41
|
+
}
|
|
42
|
+
export interface TelemetryContributor<Log extends object, Header extends object> {
|
|
43
|
+
readonly key: TelemetryKey;
|
|
44
|
+
toLogPayload(spans: SpanIndex): Log | undefined;
|
|
45
|
+
toHeaderPayload(log: Log): 0 extends 1 & Header ? never : [Header] extends [GatewayIngestible<Header>] ? Header | undefined : never;
|
|
46
|
+
}
|
|
47
|
+
export type RequestTelemetryLogPayload = {
|
|
48
|
+
proxy?: ProxyTelemetryLogPayload;
|
|
49
|
+
} & Partial<Record<Exclude<TelemetryKey, "proxy">, object>>;
|
|
50
|
+
/**
|
|
51
|
+
* Bounded structural defence in depth against casts and accidental free text.
|
|
52
|
+
* Closed-enum brands are erased at runtime; the type-level guard is the contract,
|
|
53
|
+
* while this check only enforces safe token shapes and collection bounds.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isGatewayIngestible(value: unknown): boolean;
|
|
56
|
+
/** One finalisation owner for every request-scoped telemetry contributor. */
|
|
57
|
+
export declare class RequestTelemetry {
|
|
58
|
+
#private;
|
|
59
|
+
readonly trace: TraceContext;
|
|
60
|
+
readonly contributors: Readonly<Partial<Record<TelemetryKey, {
|
|
61
|
+
readonly key: TelemetryKey;
|
|
62
|
+
toLogPayload(spans: SpanIndex): object | undefined;
|
|
63
|
+
}>>>;
|
|
64
|
+
constructor(trace: TraceContext);
|
|
65
|
+
register<Log extends object, Header extends object>(contributor: TelemetryContributor<Log, Header> & (0 extends 1 & Log ? never : 0 extends 1 & Header ? never : [Header] extends [GatewayIngestible<Header>] ? unknown : never)): void;
|
|
66
|
+
get proxy(): ProxyTelemetrySink | undefined;
|
|
67
|
+
toLogPayload(): RequestTelemetryLogPayload | undefined;
|
|
68
|
+
toHeaderValue(): string | undefined;
|
|
69
|
+
}
|
|
70
|
+
export {};
|