@apifuse/provider-sdk 2.2.0-beta.39 → 2.2.0-beta.40
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/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +1 -0
- package/dist/fixture-sanitization.d.ts +2 -0
- package/dist/fixture-sanitization.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/runtime/proxy-telemetry.d.ts +39 -1
- package/dist/runtime/proxy-telemetry.js +96 -30
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +61 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/serve-implementation.js +10 -1
- package/dist/server/trace-output.d.ts +4 -0
- package/dist/server/trace-output.js +20 -0
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/package.json +1 -1
- package/src/config/loader.ts +7 -1
- package/src/fixture-sanitization.ts +2 -1
- package/src/index.ts +5 -1
- package/src/runtime/proxy-telemetry.ts +142 -30
- package/src/runtime/trace-config.ts +77 -0
- package/src/server/index.ts +5 -1
- package/src/server/serve-implementation.ts +12 -1
- package/src/server/trace-output.ts +32 -0
- package/src/trace-sanitization.ts +63 -0
package/CHANGELOG.md
CHANGED
package/dist/config/loader.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export type SmartproxyAllocatorBodyClass = "network_error" | "http_error" | "emp
|
|
|
64
64
|
export type ProxyUserAgentSource = "declared" | "defaulted";
|
|
65
65
|
export type ProxyResolutionTelemetryEvent = {
|
|
66
66
|
provider: ProxyVendorName;
|
|
67
|
+
/** Defaults to `"ok"` when omitted. */
|
|
68
|
+
outcome?: "ok" | "error";
|
|
67
69
|
userAgentSource?: ProxyUserAgentSource;
|
|
68
70
|
protocol?: ProxyProtocol;
|
|
69
71
|
cacheStatus: ProxyCacheStatus;
|
package/dist/config/loader.js
CHANGED
|
@@ -158,6 +158,7 @@ function telemetryForPool(pool, cacheStatus, startedAt, extra = {}) {
|
|
|
158
158
|
function telemetryForFailure(cacheStatus, startedAt, extra = {}) {
|
|
159
159
|
return {
|
|
160
160
|
provider: "smartproxy",
|
|
161
|
+
outcome: "error",
|
|
161
162
|
cacheStatus,
|
|
162
163
|
cacheHit: false,
|
|
163
164
|
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
@@ -24,3 +24,5 @@ export declare function sanitizeUrlForLogs(value: string): string;
|
|
|
24
24
|
export declare function requestPathForFixture(value: string): string;
|
|
25
25
|
/** Scrubs secrets and terminal/log control characters before diagnostic text is emitted. */
|
|
26
26
|
export declare function sanitizeDiagnosticText(value: string): string;
|
|
27
|
+
/** Encodes terminal/log control characters without applying value-level secret heuristics. */
|
|
28
|
+
export declare function encodeDiagnosticControls(value: string): string;
|
|
@@ -170,7 +170,8 @@ function isCredentialBearingUrl(value) {
|
|
|
170
170
|
return false;
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
/** Encodes terminal/log control characters without applying value-level secret heuristics. */
|
|
174
|
+
export function encodeDiagnosticControls(value) {
|
|
174
175
|
let result = "";
|
|
175
176
|
for (const character of value) {
|
|
176
177
|
const code = character.codePointAt(0) ?? 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export { generateInsights } from "./runtime/insights.js";
|
|
|
31
31
|
export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
32
32
|
export type { PrevalidateResult } from "./runtime/prevalidate.js";
|
|
33
33
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
34
|
-
export type { ProxyTelemetryLogPayload } from "./runtime/proxy-telemetry.js";
|
|
34
|
+
export type { ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "./runtime/proxy-telemetry.js";
|
|
35
35
|
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";
|
|
36
36
|
export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
|
|
37
37
|
export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ProxyAttemptTelemetryEvent, ProxyCacheStatus, ProxyProtocol, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyUserAgentSource, ProxyVendorFailoverTelemetryEvent, ProxyVendorName, SmartproxyAllocatorBodyClass } from "../config/loader.js";
|
|
2
2
|
export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
|
|
3
|
-
export type
|
|
3
|
+
export type ProxyTelemetryResolvedPayload = {
|
|
4
|
+
kind: "resolved";
|
|
4
5
|
provider: ProxyVendorName;
|
|
5
6
|
userAgentSource?: ProxyUserAgentSource;
|
|
6
7
|
protocol?: ProxyProtocol;
|
|
@@ -39,6 +40,43 @@ export type ProxyTelemetryLogPayload = {
|
|
|
39
40
|
a?: number;
|
|
40
41
|
}[];
|
|
41
42
|
};
|
|
43
|
+
export type ProxyTelemetryUnresolvedPayload = {
|
|
44
|
+
kind: "unresolved";
|
|
45
|
+
/** Distinct vendors that failed resolution or recorded a failover, in order seen. */
|
|
46
|
+
vendors: ProxyVendorName[];
|
|
47
|
+
cacheStatus?: ProxyCacheStatus;
|
|
48
|
+
cacheHit?: boolean;
|
|
49
|
+
resolutionMs?: number;
|
|
50
|
+
allocatorMs?: number;
|
|
51
|
+
allocatorStatus?: number;
|
|
52
|
+
allocatorBodyClass?: SmartproxyAllocatorBodyClass;
|
|
53
|
+
allocatorAttempts?: number;
|
|
54
|
+
lockWaitMs?: number;
|
|
55
|
+
redisReadMs?: number;
|
|
56
|
+
redisWriteMs?: number;
|
|
57
|
+
poolAgeMs?: number;
|
|
58
|
+
poolExpiresInMs?: number;
|
|
59
|
+
attempts?: number;
|
|
60
|
+
refreshes?: number;
|
|
61
|
+
failovers?: {
|
|
62
|
+
v: ProxyVendorName;
|
|
63
|
+
nx?: ProxyVendorName;
|
|
64
|
+
p: ProxyVendorFailoverTelemetryEvent["phase"];
|
|
65
|
+
r: ProxyVendorFailoverTelemetryEvent["reason"];
|
|
66
|
+
a?: number;
|
|
67
|
+
}[];
|
|
68
|
+
attemptSamples?: {
|
|
69
|
+
n: number;
|
|
70
|
+
a: number;
|
|
71
|
+
i?: number;
|
|
72
|
+
h?: string;
|
|
73
|
+
o: ProxyAttemptTelemetryEvent["outcome"];
|
|
74
|
+
c?: string;
|
|
75
|
+
s?: number;
|
|
76
|
+
d?: number;
|
|
77
|
+
}[];
|
|
78
|
+
};
|
|
79
|
+
export type ProxyTelemetryLogPayload = ProxyTelemetryResolvedPayload | ProxyTelemetryUnresolvedPayload;
|
|
42
80
|
export declare class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
43
81
|
#private;
|
|
44
82
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
|
|
@@ -30,9 +30,12 @@ export class ProxyTelemetryCollector {
|
|
|
30
30
|
#events = [];
|
|
31
31
|
#attempts = [];
|
|
32
32
|
#failovers = [];
|
|
33
|
+
#unresolvedVendors = [];
|
|
33
34
|
recordProxyResolution(event) {
|
|
35
|
+
const outcome = event.outcome === "error" ? "error" : "ok";
|
|
34
36
|
this.#events.push({
|
|
35
37
|
provider: event.provider,
|
|
38
|
+
outcome,
|
|
36
39
|
...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
|
|
37
40
|
...(event.protocol ? { protocol: event.protocol } : {}),
|
|
38
41
|
cacheStatus: event.cacheStatus,
|
|
@@ -56,6 +59,9 @@ export class ProxyTelemetryCollector {
|
|
|
56
59
|
attempts: Math.max(1, Math.floor(event.attempts || 1)),
|
|
57
60
|
refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
|
|
58
61
|
});
|
|
62
|
+
if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
|
|
63
|
+
this.#unresolvedVendors.push(event.provider);
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
recordProxyVendorFailover(event) {
|
|
61
67
|
if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
|
|
@@ -67,6 +73,9 @@ export class ProxyTelemetryCollector {
|
|
|
67
73
|
reason: event.reason,
|
|
68
74
|
...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
|
|
69
75
|
});
|
|
76
|
+
if (!this.#unresolvedVendors.includes(event.vendor)) {
|
|
77
|
+
this.#unresolvedVendors.push(event.vendor);
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
80
|
recordProxyAttempt(event) {
|
|
72
81
|
if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES)
|
|
@@ -88,11 +97,90 @@ export class ProxyTelemetryCollector {
|
|
|
88
97
|
}
|
|
89
98
|
toLogPayload() {
|
|
90
99
|
const [first, ...rest] = this.#events;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
const okEvents = this.#events.filter((event) => event.outcome !== "error");
|
|
101
|
+
const attemptSamples = this.#attempts.map((attempt, index) => ({
|
|
102
|
+
n: index + 1,
|
|
103
|
+
a: attempt.attempt,
|
|
104
|
+
...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
|
|
105
|
+
...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
|
|
106
|
+
o: attempt.outcome,
|
|
107
|
+
...(attempt.errorCode ? { c: attempt.errorCode } : {}),
|
|
108
|
+
...(attempt.status === undefined ? {} : { s: attempt.status }),
|
|
109
|
+
...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
|
|
110
|
+
}));
|
|
111
|
+
const failovers = this.#failovers.map((failover) => ({
|
|
112
|
+
v: failover.vendor,
|
|
113
|
+
...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
|
|
114
|
+
p: failover.phase,
|
|
115
|
+
r: failover.reason,
|
|
116
|
+
...(failover.attempt === undefined ? {} : { a: failover.attempt }),
|
|
117
|
+
}));
|
|
118
|
+
if (okEvents.length === 0) {
|
|
119
|
+
if (!first && failovers.length === 0)
|
|
120
|
+
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
|
+
return {
|
|
144
|
+
kind: "unresolved",
|
|
145
|
+
vendors: [...this.#unresolvedVendors],
|
|
146
|
+
...(aggregate
|
|
147
|
+
? {
|
|
148
|
+
cacheStatus: aggregate.cacheStatus,
|
|
149
|
+
cacheHit: aggregate.cacheHit,
|
|
150
|
+
resolutionMs: aggregate.resolutionMs,
|
|
151
|
+
...(aggregate.allocatorMs !== undefined
|
|
152
|
+
? { allocatorMs: aggregate.allocatorMs }
|
|
153
|
+
: {}),
|
|
154
|
+
...(aggregate.allocatorStatus !== undefined
|
|
155
|
+
? { allocatorStatus: aggregate.allocatorStatus }
|
|
156
|
+
: {}),
|
|
157
|
+
...(aggregate.allocatorBodyClass !== undefined
|
|
158
|
+
? { allocatorBodyClass: aggregate.allocatorBodyClass }
|
|
159
|
+
: {}),
|
|
160
|
+
...(aggregate.allocatorAttempts !== undefined
|
|
161
|
+
? { allocatorAttempts: aggregate.allocatorAttempts }
|
|
162
|
+
: {}),
|
|
163
|
+
...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
|
|
164
|
+
...(aggregate.redisReadMs !== undefined
|
|
165
|
+
? { redisReadMs: aggregate.redisReadMs }
|
|
166
|
+
: {}),
|
|
167
|
+
...(aggregate.redisWriteMs !== undefined
|
|
168
|
+
? { redisWriteMs: aggregate.redisWriteMs }
|
|
169
|
+
: {}),
|
|
170
|
+
...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
|
|
171
|
+
...(aggregate.poolExpiresInMs !== undefined
|
|
172
|
+
? { poolExpiresInMs: aggregate.poolExpiresInMs }
|
|
173
|
+
: {}),
|
|
174
|
+
attempts: aggregate.attempts,
|
|
175
|
+
...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
|
|
176
|
+
}
|
|
177
|
+
: {}),
|
|
178
|
+
...(failovers.length > 0 ? { failovers } : {}),
|
|
179
|
+
...(attemptSamples.length > 0 ? { attemptSamples } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// The serving vendor/protocol is the last successful resolution.
|
|
183
|
+
const serving = okEvents[okEvents.length - 1] ?? first;
|
|
96
184
|
const vendors = [];
|
|
97
185
|
for (const event of this.#events) {
|
|
98
186
|
if (!vendors.includes(event.provider))
|
|
@@ -117,6 +205,7 @@ export class ProxyTelemetryCollector {
|
|
|
117
205
|
refreshes: sumOptional(acc.refreshes, event.refreshes),
|
|
118
206
|
}), first);
|
|
119
207
|
return {
|
|
208
|
+
kind: "resolved",
|
|
120
209
|
provider: serving.provider,
|
|
121
210
|
...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
|
|
122
211
|
...(serving.protocol ? { protocol: serving.protocol } : {}),
|
|
@@ -142,32 +231,9 @@ export class ProxyTelemetryCollector {
|
|
|
142
231
|
: {}),
|
|
143
232
|
attempts: aggregate.attempts,
|
|
144
233
|
...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
|
|
145
|
-
...(
|
|
146
|
-
? {
|
|
147
|
-
attemptSamples: this.#attempts.map((attempt, index) => ({
|
|
148
|
-
n: index + 1,
|
|
149
|
-
a: attempt.attempt,
|
|
150
|
-
...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
|
|
151
|
-
...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
|
|
152
|
-
o: attempt.outcome,
|
|
153
|
-
...(attempt.errorCode ? { c: attempt.errorCode } : {}),
|
|
154
|
-
...(attempt.status === undefined ? {} : { s: attempt.status }),
|
|
155
|
-
...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
|
|
156
|
-
})),
|
|
157
|
-
}
|
|
158
|
-
: {}),
|
|
234
|
+
...(attemptSamples.length > 0 ? { attemptSamples } : {}),
|
|
159
235
|
...(vendors.length > 1 ? { vendors } : {}),
|
|
160
|
-
...(
|
|
161
|
-
? {
|
|
162
|
-
failovers: this.#failovers.map((failover) => ({
|
|
163
|
-
v: failover.vendor,
|
|
164
|
-
...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
|
|
165
|
-
p: failover.phase,
|
|
166
|
-
r: failover.reason,
|
|
167
|
-
...(failover.attempt === undefined ? {} : { a: failover.attempt }),
|
|
168
|
-
})),
|
|
169
|
-
}
|
|
170
|
-
: {}),
|
|
236
|
+
...(failovers.length > 0 ? { failovers } : {}),
|
|
171
237
|
};
|
|
172
238
|
}
|
|
173
239
|
toHeaderValue() {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TraceConfig } from "../types.js";
|
|
2
|
+
/** SDK-owned environment variable names for server trace output. */
|
|
3
|
+
export declare const APIFUSE__TRACE__ENABLED = "APIFUSE__TRACE__ENABLED";
|
|
4
|
+
export declare const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
|
|
5
|
+
type EnvLike = Record<string, string | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve server trace output configuration from SDK-owned environment
|
|
8
|
+
* variables. Invalid exporter values fail closed so a provider cannot crash at
|
|
9
|
+
* startup because of an observability setting.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveTraceConfigFromEnv(env?: EnvLike): TraceConfig | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** SDK-owned environment variable names for server trace output. */
|
|
2
|
+
export const APIFUSE__TRACE__ENABLED = "APIFUSE__TRACE__ENABLED";
|
|
3
|
+
export const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
|
|
4
|
+
const TRACE_EXPORTER_LOOKUP = {
|
|
5
|
+
console: true,
|
|
6
|
+
json: true,
|
|
7
|
+
none: true,
|
|
8
|
+
};
|
|
9
|
+
const TRACE_EXPORTERS = new Set(Object.keys(TRACE_EXPORTER_LOOKUP));
|
|
10
|
+
const emittedWarnings = new Set();
|
|
11
|
+
function warnOnce(key, message) {
|
|
12
|
+
if (emittedWarnings.has(key))
|
|
13
|
+
return;
|
|
14
|
+
emittedWarnings.add(key);
|
|
15
|
+
console.warn(message);
|
|
16
|
+
}
|
|
17
|
+
function parseEnabled(raw) {
|
|
18
|
+
if (raw === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
switch (raw.trim().toLowerCase()) {
|
|
21
|
+
case "1":
|
|
22
|
+
case "true":
|
|
23
|
+
case "yes":
|
|
24
|
+
case "on":
|
|
25
|
+
return true;
|
|
26
|
+
case "0":
|
|
27
|
+
case "false":
|
|
28
|
+
case "no":
|
|
29
|
+
case "off":
|
|
30
|
+
return false;
|
|
31
|
+
default:
|
|
32
|
+
warnOnce(APIFUSE__TRACE__ENABLED, "[apifuse] Invalid APIFUSE__TRACE__ENABLED; falling back to disabled tracing.");
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve server trace output configuration from SDK-owned environment
|
|
38
|
+
* variables. Invalid exporter values fail closed so a provider cannot crash at
|
|
39
|
+
* startup because of an observability setting.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveTraceConfigFromEnv(env = process.env) {
|
|
42
|
+
const enabledRaw = env[APIFUSE__TRACE__ENABLED];
|
|
43
|
+
const exporterRaw = env[APIFUSE__TRACE__EXPORTER];
|
|
44
|
+
if (enabledRaw === undefined && exporterRaw === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const enabled = parseEnabled(enabledRaw);
|
|
48
|
+
const normalizedExporter = exporterRaw?.trim().toLowerCase();
|
|
49
|
+
const exporter = TRACE_EXPORTERS.has(normalizedExporter)
|
|
50
|
+
? normalizedExporter
|
|
51
|
+
: exporterRaw === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: "none";
|
|
54
|
+
if (exporterRaw !== undefined && exporter === "none" && normalizedExporter !== "none") {
|
|
55
|
+
warnOnce(APIFUSE__TRACE__EXPORTER, `[apifuse] Invalid APIFUSE__TRACE__EXPORTER value "${normalizedExporter ?? exporterRaw}" (OTLP is unsupported for server output); supported exporters are "console", "json", and "none"; falling back to exporter "none".`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
59
|
+
...(exporter !== undefined ? { exporter } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { ProxyCacheStatus, ProxyProtocol, ProxyUserAgentSource, ProxyVendorName, SmartproxyAllocatorBodyClass, } from "../config/loader.js";
|
|
2
|
-
export type { ProxyTelemetryLogPayload } from "../runtime/proxy-telemetry.js";
|
|
2
|
+
export type { ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "../runtime/proxy-telemetry.js";
|
|
3
3
|
export { createServerApp, createServerAppAsync, ERROR_OBSERVABILITY_HEADER, type ErrorObservabilityDetails, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
|
|
4
4
|
export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, type SelfTestAppOptions, type SelfTestAuthFlowInvoke, type SelfTestAuthFlowRoute, type SelfTestCaseResult, type SelfTestCaseStatus, type SelfTestOperationInvoke, type SelfTestRequest, SelfTestRequestSchema, type SelfTestResponse, } from "./self-test.js";
|
|
5
5
|
export { type InputDateTokenCalendar, resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
|
|
@@ -28,6 +28,7 @@ import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeStat
|
|
|
28
28
|
import { StealthCookieJar } from "../runtime/stealth-cookies.js";
|
|
29
29
|
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
30
30
|
import { createTraceContext } from "../runtime/trace.js";
|
|
31
|
+
import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
|
|
31
32
|
import { parseSchema } from "../schema.js";
|
|
32
33
|
import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER, STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER, verifyStatefulRequestSignature, } from "../stateful-signing.js";
|
|
33
34
|
import { StatefulRoutingDeadlineError } from "../stateful/errors.js";
|
|
@@ -35,6 +36,7 @@ import { getStealthProfile } from "../stealth/profiles.js";
|
|
|
35
36
|
import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
|
|
36
37
|
import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
|
|
37
38
|
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
39
|
+
import { resolveServerTraceContextOptions } from "./trace-output.js";
|
|
38
40
|
import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
|
|
39
41
|
const DEFAULT_HOST = "0.0.0.0";
|
|
40
42
|
const DEFAULT_PORT = 3000;
|
|
@@ -363,6 +365,7 @@ function resolveNativeProxyPolicy(provider) {
|
|
|
363
365
|
return undefined;
|
|
364
366
|
}
|
|
365
367
|
function createProviderContext(provider, request, operationId, options, state = createUnsupportedProviderRuntimeState(), proxyTelemetry, signal) {
|
|
368
|
+
const traceConfig = resolveTraceConfigFromEnv();
|
|
366
369
|
const baseUrl = getProviderBaseUrl(provider);
|
|
367
370
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
368
371
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
@@ -443,7 +446,13 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
443
446
|
},
|
|
444
447
|
}
|
|
445
448
|
: {}),
|
|
446
|
-
trace:
|
|
449
|
+
trace: traceConfig
|
|
450
|
+
? createTraceContext(resolveServerTraceContextOptions(traceConfig, {
|
|
451
|
+
request_id: request.requestId,
|
|
452
|
+
provider_id: provider.id,
|
|
453
|
+
operation_id: operationId,
|
|
454
|
+
}))
|
|
455
|
+
: createTraceContext(),
|
|
447
456
|
auth: createAuthStub(),
|
|
448
457
|
ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
|
|
449
458
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type CreateTraceContextOptions } from "../runtime/trace.js";
|
|
2
|
+
import type { TraceConfig } from "../types.js";
|
|
3
|
+
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
4
|
+
export declare function resolveServerTraceContextOptions(config: TraceConfig, resourceAttributes: Record<string, string>): CreateTraceContextOptions;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolveTraceContextOptions, } from "../runtime/trace.js";
|
|
2
|
+
import { sanitizeSpanForOutput } from "../trace-sanitization.js";
|
|
3
|
+
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
4
|
+
export function resolveServerTraceContextOptions(config, resourceAttributes) {
|
|
5
|
+
const resolved = resolveTraceContextOptions(config);
|
|
6
|
+
const outputEnabled = config.enabled !== false && config.exporter !== "none";
|
|
7
|
+
const consoleHook = outputEnabled && (config.exporter === "console" || config.exporter === "json")
|
|
8
|
+
? (span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
|
|
9
|
+
: undefined;
|
|
10
|
+
const onSpan = consoleHook && resolved.onSpan
|
|
11
|
+
? (span) => {
|
|
12
|
+
consoleHook(span);
|
|
13
|
+
resolved.onSpan?.(span);
|
|
14
|
+
}
|
|
15
|
+
: (consoleHook ?? resolved.onSpan);
|
|
16
|
+
return {
|
|
17
|
+
maxSpans: resolved.maxSpans,
|
|
18
|
+
onSpan,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TraceSpan } from "./types.js";
|
|
2
|
+
/** Span names are SDK-authored identifiers; retain them while preventing log injection. */
|
|
3
|
+
export declare function sanitizeSpanNameForOutput(value: string): string;
|
|
4
|
+
export declare function sanitizeTraceAttributes(attributes: Record<string, unknown>): Record<string, string | number | boolean>;
|
|
5
|
+
export declare function sanitizeSpanForOutput(span: TraceSpan, additionalAttributes?: Record<string, string>): TraceSpan;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { encodeDiagnosticControls, isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "./fixture-sanitization.js";
|
|
2
|
+
const MAX_TRACE_TEXT_LENGTH = 300;
|
|
3
|
+
function sanitizeTraceText(value) {
|
|
4
|
+
const encoded = sanitizeDiagnosticText(value);
|
|
5
|
+
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
6
|
+
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
7
|
+
: encoded;
|
|
8
|
+
}
|
|
9
|
+
/** Span names are SDK-authored identifiers; retain them while preventing log injection. */
|
|
10
|
+
export function sanitizeSpanNameForOutput(value) {
|
|
11
|
+
const encoded = encodeDiagnosticControls(value);
|
|
12
|
+
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
13
|
+
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
14
|
+
: encoded;
|
|
15
|
+
}
|
|
16
|
+
export function sanitizeTraceAttributes(attributes) {
|
|
17
|
+
return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
|
|
18
|
+
sanitizeTraceText(key),
|
|
19
|
+
isSensitiveFixtureKey(key)
|
|
20
|
+
? REDACTED_FIXTURE_VALUE
|
|
21
|
+
: typeof value === "string"
|
|
22
|
+
? sanitizeTraceText(value)
|
|
23
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
24
|
+
? value
|
|
25
|
+
: sanitizeTraceText(String(value)),
|
|
26
|
+
]));
|
|
27
|
+
}
|
|
28
|
+
export function sanitizeSpanForOutput(span, additionalAttributes) {
|
|
29
|
+
// Keep this schema explicit so future fields are not silently added to a
|
|
30
|
+
// process output path before their trust boundary has been reviewed.
|
|
31
|
+
return {
|
|
32
|
+
id: span.id,
|
|
33
|
+
name: sanitizeSpanNameForOutput(span.name),
|
|
34
|
+
startedAt: span.startedAt,
|
|
35
|
+
endedAt: span.endedAt,
|
|
36
|
+
duration_ms: span.duration_ms,
|
|
37
|
+
status: span.status,
|
|
38
|
+
attributes: sanitizeTraceAttributes({
|
|
39
|
+
...span.attributes,
|
|
40
|
+
...(additionalAttributes ?? {}),
|
|
41
|
+
}),
|
|
42
|
+
...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
|
|
43
|
+
...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
package/package.json
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -107,6 +107,8 @@ export type ProxyUserAgentSource = "declared" | "defaulted";
|
|
|
107
107
|
|
|
108
108
|
export type ProxyResolutionTelemetryEvent = {
|
|
109
109
|
provider: ProxyVendorName;
|
|
110
|
+
/** Defaults to `"ok"` when omitted. */
|
|
111
|
+
outcome?: "ok" | "error";
|
|
110
112
|
userAgentSource?: ProxyUserAgentSource;
|
|
111
113
|
protocol?: ProxyProtocol;
|
|
112
114
|
cacheStatus: ProxyCacheStatus;
|
|
@@ -370,6 +372,7 @@ function telemetryForFailure(
|
|
|
370
372
|
): ProxyResolutionTelemetryEvent {
|
|
371
373
|
return {
|
|
372
374
|
provider: "smartproxy",
|
|
375
|
+
outcome: "error",
|
|
373
376
|
cacheStatus,
|
|
374
377
|
cacheHit: false,
|
|
375
378
|
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
@@ -916,7 +919,10 @@ export function resolvePolicyTransportAttemptCap(input: {
|
|
|
916
919
|
) {
|
|
917
920
|
return budget;
|
|
918
921
|
}
|
|
919
|
-
const span = Math.min(
|
|
922
|
+
const span = Math.min(
|
|
923
|
+
maxPolicyProxyPoolSpan(input.policy as ProviderProxyPolicy),
|
|
924
|
+
resolvePolicyProxyPoolSpan(input.policy as ProviderProxyPolicy),
|
|
925
|
+
);
|
|
920
926
|
return Math.max(budget, span);
|
|
921
927
|
}
|
|
922
928
|
|
|
@@ -200,7 +200,8 @@ function isCredentialBearingUrl(value: string): boolean {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
/** Encodes terminal/log control characters without applying value-level secret heuristics. */
|
|
204
|
+
export function encodeDiagnosticControls(value: string): string {
|
|
204
205
|
let result = "";
|
|
205
206
|
for (const character of value) {
|
|
206
207
|
const code = character.codePointAt(0) ?? 0;
|
package/src/index.ts
CHANGED
|
@@ -177,7 +177,11 @@ export {
|
|
|
177
177
|
} from "./runtime/instrumentation.js";
|
|
178
178
|
export type { PrevalidateResult } from "./runtime/prevalidate.js";
|
|
179
179
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
180
|
-
export type {
|
|
180
|
+
export type {
|
|
181
|
+
ProxyTelemetryLogPayload,
|
|
182
|
+
ProxyTelemetryResolvedPayload,
|
|
183
|
+
ProxyTelemetryUnresolvedPayload,
|
|
184
|
+
} from "./runtime/proxy-telemetry.js";
|
|
181
185
|
export {
|
|
182
186
|
APIFUSE__CDP_POOL__URL,
|
|
183
187
|
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
@@ -12,7 +12,8 @@ import type {
|
|
|
12
12
|
|
|
13
13
|
export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
|
|
14
14
|
|
|
15
|
-
export type
|
|
15
|
+
export type ProxyTelemetryResolvedPayload = {
|
|
16
|
+
kind: "resolved";
|
|
16
17
|
provider: ProxyVendorName;
|
|
17
18
|
userAgentSource?: ProxyUserAgentSource;
|
|
18
19
|
protocol?: ProxyProtocol;
|
|
@@ -52,6 +53,47 @@ export type ProxyTelemetryLogPayload = {
|
|
|
52
53
|
}[];
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
export type ProxyTelemetryUnresolvedPayload = {
|
|
57
|
+
kind: "unresolved";
|
|
58
|
+
/** Distinct vendors that failed resolution or recorded a failover, in order seen. */
|
|
59
|
+
vendors: ProxyVendorName[];
|
|
60
|
+
cacheStatus?: ProxyCacheStatus;
|
|
61
|
+
cacheHit?: boolean;
|
|
62
|
+
resolutionMs?: number;
|
|
63
|
+
allocatorMs?: number;
|
|
64
|
+
allocatorStatus?: number;
|
|
65
|
+
allocatorBodyClass?: SmartproxyAllocatorBodyClass;
|
|
66
|
+
allocatorAttempts?: number;
|
|
67
|
+
lockWaitMs?: number;
|
|
68
|
+
redisReadMs?: number;
|
|
69
|
+
redisWriteMs?: number;
|
|
70
|
+
poolAgeMs?: number;
|
|
71
|
+
poolExpiresInMs?: number;
|
|
72
|
+
attempts?: number;
|
|
73
|
+
refreshes?: number;
|
|
74
|
+
failovers?: {
|
|
75
|
+
v: ProxyVendorName;
|
|
76
|
+
nx?: ProxyVendorName;
|
|
77
|
+
p: ProxyVendorFailoverTelemetryEvent["phase"];
|
|
78
|
+
r: ProxyVendorFailoverTelemetryEvent["reason"];
|
|
79
|
+
a?: number;
|
|
80
|
+
}[];
|
|
81
|
+
attemptSamples?: {
|
|
82
|
+
n: number;
|
|
83
|
+
a: number;
|
|
84
|
+
i?: number;
|
|
85
|
+
h?: string;
|
|
86
|
+
o: ProxyAttemptTelemetryEvent["outcome"];
|
|
87
|
+
c?: string;
|
|
88
|
+
s?: number;
|
|
89
|
+
d?: number;
|
|
90
|
+
}[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type ProxyTelemetryLogPayload =
|
|
94
|
+
| ProxyTelemetryResolvedPayload
|
|
95
|
+
| ProxyTelemetryUnresolvedPayload;
|
|
96
|
+
|
|
55
97
|
type ProviderTelemetryHeader = {
|
|
56
98
|
v: 1;
|
|
57
99
|
proxy?: ProxyTelemetryLogPayload;
|
|
@@ -94,10 +136,13 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
94
136
|
#events: ProxyResolutionTelemetryEvent[] = [];
|
|
95
137
|
#attempts: ProxyAttemptTelemetryEvent[] = [];
|
|
96
138
|
#failovers: ProxyVendorFailoverTelemetryEvent[] = [];
|
|
139
|
+
#unresolvedVendors: ProxyVendorName[] = [];
|
|
97
140
|
|
|
98
141
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void {
|
|
142
|
+
const outcome = event.outcome === "error" ? "error" : "ok";
|
|
99
143
|
this.#events.push({
|
|
100
144
|
provider: event.provider,
|
|
145
|
+
outcome,
|
|
101
146
|
...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
|
|
102
147
|
...(event.protocol ? { protocol: event.protocol } : {}),
|
|
103
148
|
cacheStatus: event.cacheStatus,
|
|
@@ -130,6 +175,9 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
130
175
|
refreshes:
|
|
131
176
|
event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
|
|
132
177
|
});
|
|
178
|
+
if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
|
|
179
|
+
this.#unresolvedVendors.push(event.provider);
|
|
180
|
+
}
|
|
133
181
|
}
|
|
134
182
|
|
|
135
183
|
recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void {
|
|
@@ -141,6 +189,9 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
141
189
|
reason: event.reason,
|
|
142
190
|
...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
|
|
143
191
|
});
|
|
192
|
+
if (!this.#unresolvedVendors.includes(event.vendor)) {
|
|
193
|
+
this.#unresolvedVendors.push(event.vendor);
|
|
194
|
+
}
|
|
144
195
|
}
|
|
145
196
|
|
|
146
197
|
recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void {
|
|
@@ -163,11 +214,94 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
163
214
|
|
|
164
215
|
toLogPayload(): ProxyTelemetryLogPayload | undefined {
|
|
165
216
|
const [first, ...rest] = this.#events;
|
|
166
|
-
|
|
217
|
+
const okEvents = this.#events.filter((event) => event.outcome !== "error");
|
|
218
|
+
const attemptSamples = this.#attempts.map((attempt, index) => ({
|
|
219
|
+
n: index + 1,
|
|
220
|
+
a: attempt.attempt,
|
|
221
|
+
...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
|
|
222
|
+
...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
|
|
223
|
+
o: attempt.outcome,
|
|
224
|
+
...(attempt.errorCode ? { c: attempt.errorCode } : {}),
|
|
225
|
+
...(attempt.status === undefined ? {} : { s: attempt.status }),
|
|
226
|
+
...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
|
|
227
|
+
}));
|
|
228
|
+
const failovers = this.#failovers.map((failover) => ({
|
|
229
|
+
v: failover.vendor,
|
|
230
|
+
...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
|
|
231
|
+
p: failover.phase,
|
|
232
|
+
r: failover.reason,
|
|
233
|
+
...(failover.attempt === undefined ? {} : { a: failover.attempt }),
|
|
234
|
+
}));
|
|
167
235
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
if (okEvents.length === 0) {
|
|
237
|
+
if (!first && failovers.length === 0) return undefined;
|
|
238
|
+
const failureEvents = this.#events.filter((event) => event.outcome === "error");
|
|
239
|
+
const [firstFailure, ...remainingFailures] = failureEvents;
|
|
240
|
+
const aggregate = firstFailure
|
|
241
|
+
? remainingFailures.reduce<ProxyResolutionTelemetryEvent>(
|
|
242
|
+
(acc, event) => ({
|
|
243
|
+
provider: event.provider,
|
|
244
|
+
outcome: "error",
|
|
245
|
+
cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
|
|
246
|
+
cacheHit: acc.cacheHit && event.cacheHit,
|
|
247
|
+
resolutionMs: acc.resolutionMs + event.resolutionMs,
|
|
248
|
+
allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
|
|
249
|
+
allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
|
|
250
|
+
allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
|
|
251
|
+
allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
|
|
252
|
+
lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
|
|
253
|
+
redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
|
|
254
|
+
redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
|
|
255
|
+
poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
|
|
256
|
+
poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
|
|
257
|
+
attempts: acc.attempts + event.attempts,
|
|
258
|
+
refreshes: sumOptional(acc.refreshes, event.refreshes),
|
|
259
|
+
}),
|
|
260
|
+
firstFailure,
|
|
261
|
+
)
|
|
262
|
+
: undefined;
|
|
263
|
+
return {
|
|
264
|
+
kind: "unresolved",
|
|
265
|
+
vendors: [...this.#unresolvedVendors],
|
|
266
|
+
...(aggregate
|
|
267
|
+
? {
|
|
268
|
+
cacheStatus: aggregate.cacheStatus,
|
|
269
|
+
cacheHit: aggregate.cacheHit,
|
|
270
|
+
resolutionMs: aggregate.resolutionMs,
|
|
271
|
+
...(aggregate.allocatorMs !== undefined
|
|
272
|
+
? { allocatorMs: aggregate.allocatorMs }
|
|
273
|
+
: {}),
|
|
274
|
+
...(aggregate.allocatorStatus !== undefined
|
|
275
|
+
? { allocatorStatus: aggregate.allocatorStatus }
|
|
276
|
+
: {}),
|
|
277
|
+
...(aggregate.allocatorBodyClass !== undefined
|
|
278
|
+
? { allocatorBodyClass: aggregate.allocatorBodyClass }
|
|
279
|
+
: {}),
|
|
280
|
+
...(aggregate.allocatorAttempts !== undefined
|
|
281
|
+
? { allocatorAttempts: aggregate.allocatorAttempts }
|
|
282
|
+
: {}),
|
|
283
|
+
...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
|
|
284
|
+
...(aggregate.redisReadMs !== undefined
|
|
285
|
+
? { redisReadMs: aggregate.redisReadMs }
|
|
286
|
+
: {}),
|
|
287
|
+
...(aggregate.redisWriteMs !== undefined
|
|
288
|
+
? { redisWriteMs: aggregate.redisWriteMs }
|
|
289
|
+
: {}),
|
|
290
|
+
...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
|
|
291
|
+
...(aggregate.poolExpiresInMs !== undefined
|
|
292
|
+
? { poolExpiresInMs: aggregate.poolExpiresInMs }
|
|
293
|
+
: {}),
|
|
294
|
+
attempts: aggregate.attempts,
|
|
295
|
+
...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
|
|
296
|
+
}
|
|
297
|
+
: {}),
|
|
298
|
+
...(failovers.length > 0 ? { failovers } : {}),
|
|
299
|
+
...(attemptSamples.length > 0 ? { attemptSamples } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// The serving vendor/protocol is the last successful resolution.
|
|
304
|
+
const serving = okEvents[okEvents.length - 1] ?? first;
|
|
171
305
|
const vendors: ProxyVendorName[] = [];
|
|
172
306
|
for (const event of this.#events) {
|
|
173
307
|
if (!vendors.includes(event.provider)) vendors.push(event.provider);
|
|
@@ -195,6 +329,7 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
195
329
|
first,
|
|
196
330
|
);
|
|
197
331
|
return {
|
|
332
|
+
kind: "resolved",
|
|
198
333
|
provider: serving.provider,
|
|
199
334
|
...(aggregate.userAgentSource ? { userAgentSource: aggregate.userAgentSource } : {}),
|
|
200
335
|
...(serving.protocol ? { protocol: serving.protocol } : {}),
|
|
@@ -220,32 +355,9 @@ export class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
|
220
355
|
: {}),
|
|
221
356
|
attempts: aggregate.attempts,
|
|
222
357
|
...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
|
|
223
|
-
...(
|
|
224
|
-
? {
|
|
225
|
-
attemptSamples: this.#attempts.map((attempt, index) => ({
|
|
226
|
-
n: index + 1,
|
|
227
|
-
a: attempt.attempt,
|
|
228
|
-
...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
|
|
229
|
-
...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
|
|
230
|
-
o: attempt.outcome,
|
|
231
|
-
...(attempt.errorCode ? { c: attempt.errorCode } : {}),
|
|
232
|
-
...(attempt.status === undefined ? {} : { s: attempt.status }),
|
|
233
|
-
...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
|
|
234
|
-
})),
|
|
235
|
-
}
|
|
236
|
-
: {}),
|
|
358
|
+
...(attemptSamples.length > 0 ? { attemptSamples } : {}),
|
|
237
359
|
...(vendors.length > 1 ? { vendors } : {}),
|
|
238
|
-
...(
|
|
239
|
-
? {
|
|
240
|
-
failovers: this.#failovers.map((failover) => ({
|
|
241
|
-
v: failover.vendor,
|
|
242
|
-
...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
|
|
243
|
-
p: failover.phase,
|
|
244
|
-
r: failover.reason,
|
|
245
|
-
...(failover.attempt === undefined ? {} : { a: failover.attempt }),
|
|
246
|
-
})),
|
|
247
|
-
}
|
|
248
|
-
: {}),
|
|
360
|
+
...(failovers.length > 0 ? { failovers } : {}),
|
|
249
361
|
};
|
|
250
362
|
}
|
|
251
363
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TraceConfig } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/** SDK-owned environment variable names for server trace output. */
|
|
4
|
+
export const APIFUSE__TRACE__ENABLED = "APIFUSE__TRACE__ENABLED";
|
|
5
|
+
export const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
|
|
6
|
+
|
|
7
|
+
type EnvLike = Record<string, string | undefined>;
|
|
8
|
+
|
|
9
|
+
const TRACE_EXPORTER_LOOKUP: Record<Exclude<NonNullable<TraceConfig["exporter"]>, "otlp">, true> = {
|
|
10
|
+
console: true,
|
|
11
|
+
json: true,
|
|
12
|
+
none: true,
|
|
13
|
+
};
|
|
14
|
+
const TRACE_EXPORTERS = new Set(Object.keys(TRACE_EXPORTER_LOOKUP));
|
|
15
|
+
|
|
16
|
+
const emittedWarnings = new Set<string>();
|
|
17
|
+
|
|
18
|
+
function warnOnce(key: string, message: string): void {
|
|
19
|
+
if (emittedWarnings.has(key)) return;
|
|
20
|
+
emittedWarnings.add(key);
|
|
21
|
+
console.warn(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseEnabled(raw: string | undefined): boolean | undefined {
|
|
25
|
+
if (raw === undefined) return undefined;
|
|
26
|
+
switch (raw.trim().toLowerCase()) {
|
|
27
|
+
case "1":
|
|
28
|
+
case "true":
|
|
29
|
+
case "yes":
|
|
30
|
+
case "on":
|
|
31
|
+
return true;
|
|
32
|
+
case "0":
|
|
33
|
+
case "false":
|
|
34
|
+
case "no":
|
|
35
|
+
case "off":
|
|
36
|
+
return false;
|
|
37
|
+
default:
|
|
38
|
+
warnOnce(
|
|
39
|
+
APIFUSE__TRACE__ENABLED,
|
|
40
|
+
"[apifuse] Invalid APIFUSE__TRACE__ENABLED; falling back to disabled tracing.",
|
|
41
|
+
);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve server trace output configuration from SDK-owned environment
|
|
48
|
+
* variables. Invalid exporter values fail closed so a provider cannot crash at
|
|
49
|
+
* startup because of an observability setting.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveTraceConfigFromEnv(env: EnvLike = process.env): TraceConfig | undefined {
|
|
52
|
+
const enabledRaw = env[APIFUSE__TRACE__ENABLED];
|
|
53
|
+
const exporterRaw = env[APIFUSE__TRACE__EXPORTER];
|
|
54
|
+
|
|
55
|
+
if (enabledRaw === undefined && exporterRaw === undefined) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const enabled = parseEnabled(enabledRaw);
|
|
60
|
+
const normalizedExporter = exporterRaw?.trim().toLowerCase();
|
|
61
|
+
const exporter = TRACE_EXPORTERS.has(normalizedExporter as NonNullable<TraceConfig["exporter"]>)
|
|
62
|
+
? (normalizedExporter as NonNullable<TraceConfig["exporter"]>)
|
|
63
|
+
: exporterRaw === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: "none";
|
|
66
|
+
if (exporterRaw !== undefined && exporter === "none" && normalizedExporter !== "none") {
|
|
67
|
+
warnOnce(
|
|
68
|
+
APIFUSE__TRACE__EXPORTER,
|
|
69
|
+
`[apifuse] Invalid APIFUSE__TRACE__EXPORTER value "${normalizedExporter ?? exporterRaw}" (OTLP is unsupported for server output); supported exporters are "console", "json", and "none"; falling back to exporter "none".`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
75
|
+
...(exporter !== undefined ? { exporter } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -5,7 +5,11 @@ export type {
|
|
|
5
5
|
ProxyVendorName,
|
|
6
6
|
SmartproxyAllocatorBodyClass,
|
|
7
7
|
} from "../config/loader.js";
|
|
8
|
-
export type {
|
|
8
|
+
export type {
|
|
9
|
+
ProxyTelemetryLogPayload,
|
|
10
|
+
ProxyTelemetryResolvedPayload,
|
|
11
|
+
ProxyTelemetryUnresolvedPayload,
|
|
12
|
+
} from "../runtime/proxy-telemetry.js";
|
|
9
13
|
export {
|
|
10
14
|
createServerApp,
|
|
11
15
|
createServerAppAsync,
|
|
@@ -74,6 +74,7 @@ import { StealthCookieJar } from "../runtime/stealth-cookies.js";
|
|
|
74
74
|
import type * as StealthRuntimeModule from "../runtime/stealth.js";
|
|
75
75
|
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
76
76
|
import { createTraceContext } from "../runtime/trace.js";
|
|
77
|
+
import { resolveTraceConfigFromEnv } from "../runtime/trace-config.js";
|
|
77
78
|
import { parseSchema } from "../schema.js";
|
|
78
79
|
import {
|
|
79
80
|
STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
|
|
@@ -116,6 +117,7 @@ import type {
|
|
|
116
117
|
import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
|
|
117
118
|
import type { SelfTestCancellationLogEvent } from "./self-test.js";
|
|
118
119
|
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
120
|
+
import { resolveServerTraceContextOptions } from "./trace-output.js";
|
|
119
121
|
import {
|
|
120
122
|
type AuthFlowRequest,
|
|
121
123
|
AuthFlowRequestSchema,
|
|
@@ -608,6 +610,7 @@ function createProviderContext(
|
|
|
608
610
|
proxyTelemetry?: ProxyTelemetryCollector,
|
|
609
611
|
signal?: AbortSignal,
|
|
610
612
|
): ProviderContext {
|
|
613
|
+
const traceConfig = resolveTraceConfigFromEnv();
|
|
611
614
|
const baseUrl = getProviderBaseUrl(provider);
|
|
612
615
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
613
616
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
@@ -711,7 +714,15 @@ function createProviderContext(
|
|
|
711
714
|
},
|
|
712
715
|
}
|
|
713
716
|
: {}),
|
|
714
|
-
trace:
|
|
717
|
+
trace: traceConfig
|
|
718
|
+
? createTraceContext(
|
|
719
|
+
resolveServerTraceContextOptions(traceConfig, {
|
|
720
|
+
request_id: request.requestId,
|
|
721
|
+
provider_id: provider.id,
|
|
722
|
+
operation_id: operationId,
|
|
723
|
+
}),
|
|
724
|
+
)
|
|
725
|
+
: createTraceContext(),
|
|
715
726
|
auth: createAuthStub(),
|
|
716
727
|
ocr: options.ocr ?? createOcrClientFromEnv(provider.ocr),
|
|
717
728
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveTraceContextOptions,
|
|
3
|
+
type CreateTraceContextOptions,
|
|
4
|
+
type Span,
|
|
5
|
+
} from "../runtime/trace.js";
|
|
6
|
+
import { sanitizeSpanForOutput } from "../trace-sanitization.js";
|
|
7
|
+
import type { TraceConfig } from "../types.js";
|
|
8
|
+
|
|
9
|
+
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
10
|
+
export function resolveServerTraceContextOptions(
|
|
11
|
+
config: TraceConfig,
|
|
12
|
+
resourceAttributes: Record<string, string>,
|
|
13
|
+
): CreateTraceContextOptions {
|
|
14
|
+
const resolved = resolveTraceContextOptions(config);
|
|
15
|
+
const outputEnabled = config.enabled !== false && config.exporter !== "none";
|
|
16
|
+
const consoleHook =
|
|
17
|
+
outputEnabled && (config.exporter === "console" || config.exporter === "json")
|
|
18
|
+
? (span: Span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
|
|
19
|
+
: undefined;
|
|
20
|
+
const onSpan =
|
|
21
|
+
consoleHook && resolved.onSpan
|
|
22
|
+
? (span: Span) => {
|
|
23
|
+
consoleHook(span);
|
|
24
|
+
resolved.onSpan?.(span);
|
|
25
|
+
}
|
|
26
|
+
: (consoleHook ?? resolved.onSpan);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
maxSpans: resolved.maxSpans,
|
|
30
|
+
onSpan,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encodeDiagnosticControls,
|
|
3
|
+
isSensitiveFixtureKey,
|
|
4
|
+
REDACTED_FIXTURE_VALUE,
|
|
5
|
+
sanitizeDiagnosticText,
|
|
6
|
+
} from "./fixture-sanitization.js";
|
|
7
|
+
import type { TraceSpan } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const MAX_TRACE_TEXT_LENGTH = 300;
|
|
10
|
+
|
|
11
|
+
function sanitizeTraceText(value: string): string {
|
|
12
|
+
const encoded = sanitizeDiagnosticText(value);
|
|
13
|
+
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
14
|
+
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
15
|
+
: encoded;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Span names are SDK-authored identifiers; retain them while preventing log injection. */
|
|
19
|
+
export function sanitizeSpanNameForOutput(value: string): string {
|
|
20
|
+
const encoded = encodeDiagnosticControls(value);
|
|
21
|
+
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
22
|
+
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
23
|
+
: encoded;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sanitizeTraceAttributes(
|
|
27
|
+
attributes: Record<string, unknown>,
|
|
28
|
+
): Record<string, string | number | boolean> {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.entries(attributes).map(([key, value]) => [
|
|
31
|
+
sanitizeTraceText(key),
|
|
32
|
+
isSensitiveFixtureKey(key)
|
|
33
|
+
? REDACTED_FIXTURE_VALUE
|
|
34
|
+
: typeof value === "string"
|
|
35
|
+
? sanitizeTraceText(value)
|
|
36
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
37
|
+
? value
|
|
38
|
+
: sanitizeTraceText(String(value)),
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sanitizeSpanForOutput(
|
|
44
|
+
span: TraceSpan,
|
|
45
|
+
additionalAttributes?: Record<string, string>,
|
|
46
|
+
): TraceSpan {
|
|
47
|
+
// Keep this schema explicit so future fields are not silently added to a
|
|
48
|
+
// process output path before their trust boundary has been reviewed.
|
|
49
|
+
return {
|
|
50
|
+
id: span.id,
|
|
51
|
+
name: sanitizeSpanNameForOutput(span.name),
|
|
52
|
+
startedAt: span.startedAt,
|
|
53
|
+
endedAt: span.endedAt,
|
|
54
|
+
duration_ms: span.duration_ms,
|
|
55
|
+
status: span.status,
|
|
56
|
+
attributes: sanitizeTraceAttributes({
|
|
57
|
+
...span.attributes,
|
|
58
|
+
...(additionalAttributes ?? {}),
|
|
59
|
+
}),
|
|
60
|
+
...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
|
|
61
|
+
...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|