@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.62
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 +16 -12
- package/dist/handle-meta.d.ts +18 -2
- package/dist/handle-meta.js +30 -2
- package/dist/handle.d.ts +33 -12
- package/dist/handle.js +69 -24
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lint.js +28 -8
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +2 -0
- package/dist/runtime/handle.js +15 -10
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +6 -0
- package/dist/runtime/http-telemetry-guard.js +150 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +222 -146
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/serve-implementation.d.ts +2 -0
- package/dist/server/serve-implementation.js +34 -35
- package/package.json +1 -1
- package/src/handle-meta.ts +39 -4
- package/src/handle.ts +121 -38
- package/src/index.ts +19 -0
- package/src/lint.ts +30 -7
- package/src/provider.ts +10 -0
- package/src/runtime/handle.ts +20 -10
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +155 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +295 -173
- package/src/server/index.ts +13 -0
- package/src/server/serve-implementation.ts +41 -58
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { closedEnum, } from "./request-telemetry.js";
|
|
3
|
+
export function httpTelemetryErrorCode(code) {
|
|
4
|
+
switch (code) {
|
|
5
|
+
case "transport_network_error":
|
|
6
|
+
case "transport_timeout":
|
|
7
|
+
case "transport_cancelled":
|
|
8
|
+
case "upstream_http_error":
|
|
9
|
+
return code;
|
|
10
|
+
default:
|
|
11
|
+
return "other";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const MAX_ATTEMPT_SAMPLES = 24;
|
|
15
|
+
const MAX_DIAGNOSTIC_TEXT = 300;
|
|
16
|
+
function integer(value) {
|
|
17
|
+
return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
|
|
18
|
+
}
|
|
19
|
+
function saturatingAdd(a, b) {
|
|
20
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
21
|
+
return Number.MAX_SAFE_INTEGER;
|
|
22
|
+
return Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
|
|
23
|
+
}
|
|
24
|
+
function boundedText(value, redact) {
|
|
25
|
+
let result;
|
|
26
|
+
try {
|
|
27
|
+
result = redact?.(value) ?? value;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "[REDACTION_FAILED]";
|
|
31
|
+
}
|
|
32
|
+
// Detach caller text through independent UTF-16 storage, preserving lone surrogates.
|
|
33
|
+
const length = Math.min(result.length, MAX_DIAGNOSTIC_TEXT);
|
|
34
|
+
const units = Buffer.alloc(length * 2);
|
|
35
|
+
for (let index = 0; index < length; index++)
|
|
36
|
+
units.writeUInt16LE(result.charCodeAt(index), index * 2);
|
|
37
|
+
return units.toString("utf16le");
|
|
38
|
+
}
|
|
39
|
+
export class HttpTelemetryCollector {
|
|
40
|
+
key = "http";
|
|
41
|
+
#redact;
|
|
42
|
+
#telemetryFailed = false;
|
|
43
|
+
#attempts = 0;
|
|
44
|
+
#retries = 0;
|
|
45
|
+
#timeouts = 0;
|
|
46
|
+
#proxyUsed = false;
|
|
47
|
+
#lastStatus;
|
|
48
|
+
#retryPreset;
|
|
49
|
+
#lastErrorCode;
|
|
50
|
+
#samples = [];
|
|
51
|
+
#dropped = 0;
|
|
52
|
+
#ms = 0;
|
|
53
|
+
#retry;
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.#redact = options.redact;
|
|
56
|
+
}
|
|
57
|
+
markTelemetryFailed() {
|
|
58
|
+
this.#telemetryFailed = true;
|
|
59
|
+
}
|
|
60
|
+
startRequest(options) {
|
|
61
|
+
const preset = options.retryPreset;
|
|
62
|
+
let attempts = 0;
|
|
63
|
+
let retries = 0;
|
|
64
|
+
let lastErrorCode;
|
|
65
|
+
let lastStatus;
|
|
66
|
+
let retry;
|
|
67
|
+
let finished = false;
|
|
68
|
+
return {
|
|
69
|
+
recordAttempt: (event) => {
|
|
70
|
+
if (attempts > 0) {
|
|
71
|
+
retries = saturatingAdd(retries, 1);
|
|
72
|
+
this.#retries = saturatingAdd(this.#retries, 1);
|
|
73
|
+
}
|
|
74
|
+
attempts = saturatingAdd(attempts, 1);
|
|
75
|
+
this.#attempts = saturatingAdd(this.#attempts, 1);
|
|
76
|
+
if (event.e === "transport_timeout")
|
|
77
|
+
this.#timeouts = saturatingAdd(this.#timeouts, 1);
|
|
78
|
+
this.#proxyUsed ||= event.proxyUsed;
|
|
79
|
+
this.#retryPreset = preset;
|
|
80
|
+
if (event.status !== undefined)
|
|
81
|
+
this.#lastStatus = integer(event.status);
|
|
82
|
+
if (event.e)
|
|
83
|
+
this.#lastErrorCode = event.e;
|
|
84
|
+
if (this.#samples.length < MAX_ATTEMPT_SAMPLES) {
|
|
85
|
+
const text = (value) => boundedText(value, this.#redact);
|
|
86
|
+
this.#samples.push({
|
|
87
|
+
n: attempts,
|
|
88
|
+
ms: saturatingAdd(0, event.ms),
|
|
89
|
+
...(event.status === undefined ? {} : { status: integer(event.status) }),
|
|
90
|
+
...(event.e ? { e: event.e } : {}),
|
|
91
|
+
...(event.diagnostics
|
|
92
|
+
? {
|
|
93
|
+
diagnostics: {
|
|
94
|
+
name: text(event.diagnostics.name),
|
|
95
|
+
message: text(event.diagnostics.message),
|
|
96
|
+
...(event.diagnostics.cause
|
|
97
|
+
? {
|
|
98
|
+
cause: {
|
|
99
|
+
name: text(event.diagnostics.cause.name),
|
|
100
|
+
message: text(event.diagnostics.cause.message),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
this.#dropped = saturatingAdd(this.#dropped, 1);
|
|
111
|
+
if (event.e) {
|
|
112
|
+
lastStatus = event.status;
|
|
113
|
+
// Historically status-retry outcomes set lastStatus only. Preserve those pins.
|
|
114
|
+
if (!event.statusRetry)
|
|
115
|
+
lastErrorCode = event.e;
|
|
116
|
+
}
|
|
117
|
+
else if (attempts > 1) {
|
|
118
|
+
retry = {
|
|
119
|
+
attempts,
|
|
120
|
+
retries,
|
|
121
|
+
...(preset ? { preset: closedEnum(preset) } : {}),
|
|
122
|
+
transport: closedEnum("native"),
|
|
123
|
+
...(lastErrorCode ? { lastErrorCode: closedEnum(lastErrorCode) } : {}),
|
|
124
|
+
...(lastStatus ? { lastStatus: integer(lastStatus) } : {}),
|
|
125
|
+
};
|
|
126
|
+
this.#retry = retry;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
finish: (ms) => {
|
|
130
|
+
if (!finished && attempts > 0)
|
|
131
|
+
this.#ms = saturatingAdd(this.#ms, ms);
|
|
132
|
+
finished = true;
|
|
133
|
+
},
|
|
134
|
+
toTenantRetryPayload: () => (!this.#telemetryFailed && retry ? { ...retry } : undefined),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
toTenantRetryPayload() {
|
|
138
|
+
return !this.#telemetryFailed && this.#retry ? { ...this.#retry } : undefined;
|
|
139
|
+
}
|
|
140
|
+
toLogPayload() {
|
|
141
|
+
if (this.#attempts === 0 && !this.#telemetryFailed)
|
|
142
|
+
return undefined;
|
|
143
|
+
return {
|
|
144
|
+
...(this.#telemetryFailed ? { telemetryFailed: true } : {}),
|
|
145
|
+
attempts: this.#attempts,
|
|
146
|
+
retries: this.#retries,
|
|
147
|
+
timeouts: this.#timeouts,
|
|
148
|
+
proxyUsed: this.#proxyUsed,
|
|
149
|
+
...(this.#lastStatus === undefined ? {} : { lastStatus: this.#lastStatus }),
|
|
150
|
+
...(this.#retryPreset ? { retryPreset: this.#retryPreset } : {}),
|
|
151
|
+
transport: "native",
|
|
152
|
+
...(this.#lastErrorCode ? { lastErrorCode: this.#lastErrorCode } : {}),
|
|
153
|
+
attemptSamples: this.#samples.map((sample) => ({
|
|
154
|
+
...sample,
|
|
155
|
+
...(sample.diagnostics
|
|
156
|
+
? {
|
|
157
|
+
diagnostics: {
|
|
158
|
+
...sample.diagnostics,
|
|
159
|
+
...(sample.diagnostics.cause ? { cause: { ...sample.diagnostics.cause } } : {}),
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
: {}),
|
|
163
|
+
})),
|
|
164
|
+
dropped: this.#dropped,
|
|
165
|
+
ms: this.#ms,
|
|
166
|
+
...(this.#retry ? { retry: { ...this.#retry } } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
toHeaderPayload(log) {
|
|
170
|
+
return {
|
|
171
|
+
attempts: log.attempts,
|
|
172
|
+
retries: log.retries,
|
|
173
|
+
timeouts: log.timeouts,
|
|
174
|
+
proxyUsed: log.proxyUsed,
|
|
175
|
+
...(log.lastStatus === undefined ? {} : { lastStatus: log.lastStatus }),
|
|
176
|
+
...(log.retryPreset ? { retryPreset: closedEnum(log.retryPreset) } : {}),
|
|
177
|
+
transport: closedEnum(log.transport),
|
|
178
|
+
...(log.lastErrorCode ? { lastErrorCode: closedEnum(log.lastErrorCode) } : {}),
|
|
179
|
+
attemptSamples: log.attemptSamples.map(({ n, ms, status, e }) => ({
|
|
180
|
+
n,
|
|
181
|
+
ms,
|
|
182
|
+
...(status === undefined ? {} : { status }),
|
|
183
|
+
...(e ? { e: closedEnum(e) } : {}),
|
|
184
|
+
})),
|
|
185
|
+
dropped: log.dropped,
|
|
186
|
+
ms: log.ms,
|
|
187
|
+
...(log.retry ? { retry: { ...log.retry } } : {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
package/dist/runtime/http.d.ts
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
2
2
|
import type { HttpClient, HttpRetrySummary } from "../types.js";
|
|
3
|
+
import { type HttpTelemetrySink } from "./http-telemetry.js";
|
|
3
4
|
export type HttpClientOptions = ProxyResolutionOptions & {
|
|
4
5
|
warn?: (message: string) => void;
|
|
5
6
|
userAgent?: string;
|
|
6
7
|
onRetrySummary?: (summary: HttpRetrySummary) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Synchronous observer for HTTP attempts, retries, timeouts, status, duration,
|
|
10
|
+
* and observed proxy use. Observer failures never change transport behavior.
|
|
11
|
+
* All hooks and their property reads are guarded, including `onRetrySummary`
|
|
12
|
+
* and the failure reporter. Invalid asynchronous results are ignored and never
|
|
13
|
+
* awaited. Observer failures set the log-only `telemetryFailed` marker; the
|
|
14
|
+
* marker and redacted error diagnostics are excluded from headers and tenant
|
|
15
|
+
* metadata. Tenant `meta.retry` retains its existing response shape.
|
|
16
|
+
*
|
|
17
|
+
* After `ProviderEngine.attach`, the server wraps supported host HTTP bindings
|
|
18
|
+
* with the request collector. The adapter preserves arguments, direct-call,
|
|
19
|
+
* explicit `.call`/`.apply`, and unbound receivers, plus the original returned
|
|
20
|
+
* promise, response, stream, and error. Streams and SSE are observed at headers
|
|
21
|
+
* without consuming their bodies. SDK clients report every observed attempt
|
|
22
|
+
* and keep any observer configured here; routing is scoped to each call.
|
|
23
|
+
*
|
|
24
|
+
* Binding an existing adapter reuses its identity and replaces its collector:
|
|
25
|
+
* the latest bind receives subsequent calls, the earlier collector receives no
|
|
26
|
+
* further calls, and in-flight calls keep the collector captured at invocation.
|
|
27
|
+
* An opaque host invocation is one observed attempt with zero observed retries;
|
|
28
|
+
* its undisclosed retries and proxy settings cannot be inferred, and tenant
|
|
29
|
+
* `meta.retry` is omitted. Status, duration, and error class remain available
|
|
30
|
+
* in logs and telemetry headers.
|
|
31
|
+
*
|
|
32
|
+
* Non-object bindings and objects without a callable `request`, `get`, `post`,
|
|
33
|
+
* `put`, `delete`, `stream`, or `sse` method are left untouched, with no `http`
|
|
34
|
+
* telemetry sibling and one process-wide warning for this reason:
|
|
35
|
+
* `[apifuse] http telemetry not attached; reason=unsupported_binding_shape`.
|
|
36
|
+
*/
|
|
37
|
+
httpTelemetry?: HttpTelemetrySink;
|
|
7
38
|
signal?: AbortSignal;
|
|
8
39
|
};
|
|
9
40
|
export declare function createHttpClient(baseUrl?: string, clientOptions?: HttpClientOptions): HttpClient;
|