ydb-qdrant 8.1.0 → 9.0.3
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 +20 -18
- package/dist/SmokeTest.js +2 -2
- package/dist/compute/ComputePool.d.ts +5 -0
- package/dist/compute/ComputePool.js +64 -0
- package/dist/compute/ComputeWorker.d.ts +36 -0
- package/dist/compute/ComputeWorker.js +84 -0
- package/dist/config/env.d.ts +24 -7
- package/dist/config/env.js +65 -35
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -2
- package/dist/logging/DeployLogFormatter.d.ts +2 -0
- package/dist/logging/DeployLogFormatter.js +131 -0
- package/dist/logging/logger.js +13 -1
- package/dist/logging/requestContext.d.ts +17 -0
- package/dist/logging/requestContext.js +43 -0
- package/dist/middleware/requestLogger.js +134 -6
- package/dist/middleware/upsertBodyPhase.d.ts +6 -0
- package/dist/middleware/upsertBodyPhase.js +184 -0
- package/dist/middleware/upsertRequestTimeout.d.ts +16 -0
- package/dist/middleware/upsertRequestTimeout.js +158 -0
- package/dist/package/api.d.ts +20 -12
- package/dist/package/api.js +57 -28
- package/dist/qdrant/QdrantRestTypes.d.ts +4 -0
- package/dist/qdrant/Requests.d.ts +97 -0
- package/dist/qdrant/Requests.js +72 -0
- package/dist/repositories/collectionsRepo.d.ts +18 -2
- package/dist/repositories/collectionsRepo.js +103 -7
- package/dist/repositories/collectionsRepo.one-table.d.ts +4 -3
- package/dist/repositories/collectionsRepo.one-table.js +99 -36
- package/dist/repositories/collectionsRepo.shared.d.ts +2 -2
- package/dist/repositories/collectionsRepo.shared.js +9 -4
- package/dist/repositories/pointsRepo.d.ts +6 -4
- package/dist/repositories/pointsRepo.js +8 -7
- package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Delete.js +157 -60
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +7 -5
- package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +44 -13
- package/dist/repositories/pointsRepo.one-table/Retrieve.d.ts +6 -0
- package/dist/repositories/pointsRepo.one-table/Retrieve.js +69 -0
- package/dist/repositories/pointsRepo.one-table/Search.d.ts +2 -3
- package/dist/repositories/pointsRepo.one-table/Search.js +102 -124
- package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
- package/dist/repositories/pointsRepo.one-table/Upsert.js +244 -48
- package/dist/repositories/pointsRepo.one-table.d.ts +1 -0
- package/dist/repositories/pointsRepo.one-table.js +1 -0
- package/dist/routes/collections.js +45 -36
- package/dist/routes/points.js +145 -56
- package/dist/server.js +42 -6
- package/dist/services/CollectionService.d.ts +7 -5
- package/dist/services/CollectionService.js +12 -9
- package/dist/services/CollectionService.one-table.js +1 -2
- package/dist/services/CollectionService.shared.d.ts +6 -5
- package/dist/services/CollectionService.shared.js +28 -12
- package/dist/services/PointsService.d.ts +8 -0
- package/dist/services/PointsService.js +132 -15
- package/dist/types.d.ts +4 -94
- package/dist/types.js +1 -54
- package/dist/utils/EnvParsers.d.ts +5 -0
- package/dist/utils/EnvParsers.js +30 -0
- package/dist/utils/PayloadSign.d.ts +4 -0
- package/dist/utils/PayloadSign.js +18 -0
- package/dist/utils/distance.d.ts +1 -12
- package/dist/utils/distance.js +0 -21
- package/dist/utils/pathPrefix.d.ts +3 -0
- package/dist/utils/pathPrefix.js +47 -0
- package/dist/utils/prefixExpansion.d.ts +1 -0
- package/dist/utils/prefixExpansion.js +11 -0
- package/dist/utils/qdrantResponse.d.ts +13 -0
- package/dist/utils/qdrantResponse.js +12 -0
- package/dist/utils/requestIdentity.d.ts +8 -0
- package/dist/utils/requestIdentity.js +52 -0
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +55 -11
- package/dist/utils/tenant.d.ts +12 -6
- package/dist/utils/tenant.js +41 -32
- package/dist/utils/vectorBinary.d.ts +0 -1
- package/dist/utils/vectorBinary.js +0 -98
- package/dist/utils/ydbErrors.d.ts +1 -0
- package/dist/utils/ydbErrors.js +14 -0
- package/dist/ydb/bootstrapMetaTable.js +14 -2
- package/dist/ydb/client.d.ts +10 -2
- package/dist/ydb/client.js +83 -24
- package/dist/ydb/helpers.d.ts +0 -1
- package/dist/ydb/helpers.js +1 -2
- package/dist/ydb/schema.d.ts +2 -0
- package/dist/ydb/schema.js +84 -7
- package/package.json +10 -5
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
const RESERVED_FIELDS = new Set([
|
|
3
|
+
"msg",
|
|
4
|
+
"message",
|
|
5
|
+
"level",
|
|
6
|
+
"levelStr",
|
|
7
|
+
"@fields",
|
|
8
|
+
"stackTrace",
|
|
9
|
+
"stack",
|
|
10
|
+
]);
|
|
11
|
+
function isObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null;
|
|
13
|
+
}
|
|
14
|
+
function isPinoLine(value) {
|
|
15
|
+
if (!isObject(value)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return "hostname" in value && "pid" in value;
|
|
19
|
+
}
|
|
20
|
+
function mapPinoLevelToDeploy(level) {
|
|
21
|
+
if (typeof level !== "number") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
switch (level) {
|
|
25
|
+
case 60:
|
|
26
|
+
case 50:
|
|
27
|
+
return { level: 40000, levelStr: "ERROR" };
|
|
28
|
+
case 40:
|
|
29
|
+
return { level: 30000, levelStr: "WARNING" };
|
|
30
|
+
case 30:
|
|
31
|
+
return { level: 20000, levelStr: "INFO" };
|
|
32
|
+
case 20:
|
|
33
|
+
case 10:
|
|
34
|
+
return { level: 10000, levelStr: "DEBUG" };
|
|
35
|
+
default:
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getDeployMessage(obj) {
|
|
40
|
+
if (typeof obj.msg === "string") {
|
|
41
|
+
return obj.msg;
|
|
42
|
+
}
|
|
43
|
+
if (typeof obj.message === "string") {
|
|
44
|
+
return obj.message;
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
function chunkToUtf8Text(chunk) {
|
|
49
|
+
if (typeof chunk === "string") {
|
|
50
|
+
return chunk;
|
|
51
|
+
}
|
|
52
|
+
if (Buffer.isBuffer(chunk)) {
|
|
53
|
+
return chunk.toString("utf8");
|
|
54
|
+
}
|
|
55
|
+
if (chunk instanceof Uint8Array) {
|
|
56
|
+
return Buffer.from(chunk).toString("utf8");
|
|
57
|
+
}
|
|
58
|
+
return String(chunk);
|
|
59
|
+
}
|
|
60
|
+
function convertToDeployLine(obj) {
|
|
61
|
+
const fields = {};
|
|
62
|
+
const msg = getDeployMessage(obj);
|
|
63
|
+
const mapped = mapPinoLevelToDeploy(obj.level);
|
|
64
|
+
const log = {
|
|
65
|
+
"@fields": fields,
|
|
66
|
+
message: msg,
|
|
67
|
+
...(mapped ? { level: mapped.level, levelStr: mapped.levelStr } : {}),
|
|
68
|
+
};
|
|
69
|
+
if (typeof obj.stack === "string" && obj.stack.length > 0) {
|
|
70
|
+
log.stackTrace = obj.stack;
|
|
71
|
+
}
|
|
72
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
73
|
+
if (!RESERVED_FIELDS.has(key)) {
|
|
74
|
+
fields[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return JSON.stringify(log);
|
|
78
|
+
}
|
|
79
|
+
export function createDeployLogFormatter() {
|
|
80
|
+
let buffered = "";
|
|
81
|
+
return new Transform({
|
|
82
|
+
readableObjectMode: false,
|
|
83
|
+
writableObjectMode: false,
|
|
84
|
+
transform(chunk, _encoding, callback) {
|
|
85
|
+
const text = chunkToUtf8Text(chunk);
|
|
86
|
+
buffered += text;
|
|
87
|
+
let nl;
|
|
88
|
+
while ((nl = buffered.indexOf("\n")) !== -1) {
|
|
89
|
+
const line = buffered.slice(0, nl);
|
|
90
|
+
buffered = buffered.slice(nl + 1);
|
|
91
|
+
if (line.trim().length === 0) {
|
|
92
|
+
this.push("\n");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(line);
|
|
97
|
+
if (!isPinoLine(parsed)) {
|
|
98
|
+
this.push(line + "\n");
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
this.push(convertToDeployLine(parsed) + "\n");
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
this.push(line + "\n");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
callback();
|
|
108
|
+
},
|
|
109
|
+
flush(callback) {
|
|
110
|
+
if (buffered.trim().length === 0) {
|
|
111
|
+
callback();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const line = buffered;
|
|
115
|
+
buffered = "";
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(line);
|
|
118
|
+
if (!isPinoLine(parsed)) {
|
|
119
|
+
this.push(line + "\n");
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.push(convertToDeployLine(parsed) + "\n");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
this.push(line + "\n");
|
|
127
|
+
}
|
|
128
|
+
callback();
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
package/dist/logging/logger.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
2
|
import { LOG_LEVEL } from "../config/env.js";
|
|
3
|
-
|
|
3
|
+
import { createDeployLogFormatter } from "./DeployLogFormatter.js";
|
|
4
|
+
import { getRequestContextLogFields } from "./requestContext.js";
|
|
5
|
+
const deployFormatter = createDeployLogFormatter();
|
|
6
|
+
deployFormatter.pipe(process.stdout);
|
|
7
|
+
export const logger = pino({
|
|
8
|
+
level: LOG_LEVEL,
|
|
9
|
+
serializers: {
|
|
10
|
+
err: pino.stdSerializers.err,
|
|
11
|
+
},
|
|
12
|
+
mixin() {
|
|
13
|
+
return getRequestContextLogFields();
|
|
14
|
+
},
|
|
15
|
+
}, deployFormatter);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type RequestContext = {
|
|
2
|
+
requestId: string;
|
|
3
|
+
method: string;
|
|
4
|
+
url: string;
|
|
5
|
+
traceparent?: string;
|
|
6
|
+
amznTraceId?: string;
|
|
7
|
+
requestStartNs?: bigint;
|
|
8
|
+
};
|
|
9
|
+
export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
|
|
10
|
+
export declare function getRequestContext(): RequestContext | undefined;
|
|
11
|
+
export declare function getRequestContextLogFields(): Partial<RequestContext> | Record<string, never>;
|
|
12
|
+
export declare function getMonotonicTimeNs(): bigint;
|
|
13
|
+
export declare function elapsedMsBetween(startNs: bigint, endNs: bigint): number;
|
|
14
|
+
export declare function elapsedMsSince(startNs: bigint): number;
|
|
15
|
+
export declare function getRequestStartNs(): bigint | undefined;
|
|
16
|
+
export declare function elapsedMsFromRequestStart(markNs: bigint | undefined): number | undefined;
|
|
17
|
+
export declare function getElapsedMsSinceRequestStart(): number | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const requestContextStorage = new AsyncLocalStorage();
|
|
3
|
+
export function runWithRequestContext(context, fn) {
|
|
4
|
+
return requestContextStorage.run(context, fn);
|
|
5
|
+
}
|
|
6
|
+
export function getRequestContext() {
|
|
7
|
+
return requestContextStorage.getStore();
|
|
8
|
+
}
|
|
9
|
+
export function getRequestContextLogFields() {
|
|
10
|
+
const context = getRequestContext();
|
|
11
|
+
if (!context) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
const { requestStartNs: _requestStartNs, ...publicFields } = context;
|
|
15
|
+
void _requestStartNs;
|
|
16
|
+
return publicFields;
|
|
17
|
+
}
|
|
18
|
+
export function getMonotonicTimeNs() {
|
|
19
|
+
return process.hrtime.bigint();
|
|
20
|
+
}
|
|
21
|
+
export function elapsedMsBetween(startNs, endNs) {
|
|
22
|
+
return Number((endNs - startNs) / 1000000n);
|
|
23
|
+
}
|
|
24
|
+
export function elapsedMsSince(startNs) {
|
|
25
|
+
return elapsedMsBetween(startNs, getMonotonicTimeNs());
|
|
26
|
+
}
|
|
27
|
+
export function getRequestStartNs() {
|
|
28
|
+
return getRequestContext()?.requestStartNs;
|
|
29
|
+
}
|
|
30
|
+
export function elapsedMsFromRequestStart(markNs) {
|
|
31
|
+
const requestStartNs = getRequestStartNs();
|
|
32
|
+
if (requestStartNs === undefined || markNs === undefined) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return elapsedMsBetween(requestStartNs, markNs);
|
|
36
|
+
}
|
|
37
|
+
export function getElapsedMsSinceRequestStart() {
|
|
38
|
+
const requestStartNs = getRequestStartNs();
|
|
39
|
+
if (requestStartNs === undefined) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return elapsedMsSince(requestStartNs);
|
|
43
|
+
}
|
|
@@ -1,14 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { logger } from "../logging/logger.js";
|
|
3
|
+
import { getUpsertRequestTimeoutPhase, getUpsertRequestTimeoutMs, isUpsertRequestTimedOut, } from "./upsertRequestTimeout.js";
|
|
4
|
+
import { getUpsertBodyPhaseTimeoutLogFields } from "./upsertBodyPhase.js";
|
|
5
|
+
import { runWithRequestContext, getMonotonicTimeNs, } from "../logging/requestContext.js";
|
|
6
|
+
const MAX_REQUEST_ID_LENGTH = 128;
|
|
7
|
+
const SAFE_REQUEST_ID_RE = /^[A-Za-z0-9._:-]+$/;
|
|
8
|
+
function normalizeRequestId(value) {
|
|
9
|
+
const trimmed = value?.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (trimmed.length > MAX_REQUEST_ID_LENGTH) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return SAFE_REQUEST_ID_RE.test(trimmed) ? trimmed : undefined;
|
|
17
|
+
}
|
|
18
|
+
function parseClientIpFromXForwardedFor(value) {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const first = value.split(",")[0]?.trim();
|
|
23
|
+
return first && first.length > 0 ? first : undefined;
|
|
24
|
+
}
|
|
25
|
+
function parseXForwardedForChainLen(value) {
|
|
26
|
+
if (!value) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((s) => s.trim())
|
|
32
|
+
.filter((s) => s.length > 0).length;
|
|
33
|
+
}
|
|
2
34
|
export function requestLogger(req, res, next) {
|
|
3
35
|
const start = Date.now();
|
|
4
|
-
const
|
|
36
|
+
const traceparent = req.header("traceparent") ?? undefined;
|
|
37
|
+
const amznTraceId = req.header("X-Amzn-Trace-Id") ?? undefined;
|
|
38
|
+
const requestIdFromHeader = normalizeRequestId(req.header("X-Request-Id") ?? undefined);
|
|
39
|
+
const requestId = requestIdFromHeader ?? randomUUID();
|
|
40
|
+
res.setHeader("X-Request-Id", requestId);
|
|
41
|
+
const userAgent = req.header("User-Agent") ?? undefined;
|
|
5
42
|
const { method, originalUrl } = req;
|
|
43
|
+
const requestContext = {
|
|
44
|
+
requestId,
|
|
45
|
+
method,
|
|
46
|
+
url: originalUrl,
|
|
47
|
+
traceparent,
|
|
48
|
+
amznTraceId,
|
|
49
|
+
requestStartNs: getMonotonicTimeNs(),
|
|
50
|
+
};
|
|
6
51
|
const contentLength = req.header("content-length");
|
|
52
|
+
const xForwardedFor = req.header("x-forwarded-for");
|
|
7
53
|
const queryKeys = Object.keys(req.query || {});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
54
|
+
let finished = false;
|
|
55
|
+
let aborted = false;
|
|
56
|
+
let closedLogged = false;
|
|
57
|
+
runWithRequestContext(requestContext, () => {
|
|
58
|
+
logger.info({
|
|
59
|
+
userAgent,
|
|
60
|
+
contentLength,
|
|
61
|
+
clientIp: parseClientIpFromXForwardedFor(xForwardedFor) ??
|
|
62
|
+
req.socket.remoteAddress ??
|
|
63
|
+
undefined,
|
|
64
|
+
ipChainLen: parseXForwardedForChainLen(xForwardedFor),
|
|
65
|
+
hasXForwardedFor: xForwardedFor !== undefined,
|
|
66
|
+
queryKeys,
|
|
67
|
+
}, "req");
|
|
68
|
+
req.on("aborted", () => {
|
|
69
|
+
aborted = true;
|
|
70
|
+
});
|
|
71
|
+
res.on("finish", () => {
|
|
72
|
+
runWithRequestContext(requestContext, () => {
|
|
73
|
+
finished = true;
|
|
74
|
+
const ms = Date.now() - start;
|
|
75
|
+
const timeoutMs = getUpsertRequestTimeoutMs(req);
|
|
76
|
+
const timeoutPhase = getUpsertRequestTimeoutPhase(req);
|
|
77
|
+
if (isUpsertRequestTimedOut(req) && timeoutMs !== undefined) {
|
|
78
|
+
logger.warn({
|
|
79
|
+
userAgent,
|
|
80
|
+
contentLength,
|
|
81
|
+
status: res.statusCode,
|
|
82
|
+
ms,
|
|
83
|
+
bytesRead: req.socket.bytesRead,
|
|
84
|
+
timeoutMs,
|
|
85
|
+
timeoutPhase,
|
|
86
|
+
...getUpsertBodyPhaseTimeoutLogFields(req),
|
|
87
|
+
}, "req timed out");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const payload = {
|
|
91
|
+
userAgent,
|
|
92
|
+
status: res.statusCode,
|
|
93
|
+
ms,
|
|
94
|
+
};
|
|
95
|
+
if (res.statusCode >= 500) {
|
|
96
|
+
logger.error(payload, "res");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (res.statusCode >= 300) {
|
|
100
|
+
logger.warn(payload, "res");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
logger.info(payload, "res");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
res.on("close", () => {
|
|
107
|
+
runWithRequestContext(requestContext, () => {
|
|
108
|
+
if (finished || closedLogged) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
closedLogged = true;
|
|
112
|
+
const ms = Date.now() - start;
|
|
113
|
+
const bytesRead = req.socket.bytesRead;
|
|
114
|
+
const payload = {
|
|
115
|
+
userAgent,
|
|
116
|
+
contentLength,
|
|
117
|
+
status: res.statusCode,
|
|
118
|
+
ms,
|
|
119
|
+
bytesRead,
|
|
120
|
+
aborted,
|
|
121
|
+
};
|
|
122
|
+
const timeoutMs = getUpsertRequestTimeoutMs(req);
|
|
123
|
+
const timeoutPhase = getUpsertRequestTimeoutPhase(req);
|
|
124
|
+
if (isUpsertRequestTimedOut(req) && timeoutMs !== undefined) {
|
|
125
|
+
logger.warn({
|
|
126
|
+
...payload,
|
|
127
|
+
timeoutMs,
|
|
128
|
+
timeoutPhase,
|
|
129
|
+
...getUpsertBodyPhaseTimeoutLogFields(req),
|
|
130
|
+
}, "req timed out");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (aborted) {
|
|
134
|
+
logger.warn(payload, "req aborted");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
logger.warn(payload, "req closed before response finished");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
next();
|
|
12
141
|
});
|
|
13
|
-
next();
|
|
14
142
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
export declare function instrumentUpsertRequestBody(req: Request, _res: Response, next: NextFunction): void;
|
|
3
|
+
export declare function verifyUpsertRequestBody(req: Request, _res: Response, buf: Buffer): void;
|
|
4
|
+
export declare function logUpsertBodyPhase(req: Request, res: Response, next: NextFunction): void;
|
|
5
|
+
export declare function logUpsertBodyPhaseError(err: unknown, req: Request, _res: Response, next: NextFunction): void;
|
|
6
|
+
export declare function getUpsertBodyPhaseTimeoutLogFields(req: Request): Record<string, unknown>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { UPSERT_BODY_TIMEOUT_MS } from "../config/env.js";
|
|
2
|
+
import { logger } from "../logging/logger.js";
|
|
3
|
+
import { elapsedMsBetween, elapsedMsFromRequestStart, getRequestStartNs, getMonotonicTimeNs, } from "../logging/requestContext.js";
|
|
4
|
+
import { isUpsertRequestTarget, isUpsertRequestTimedOut, markUpsertBodyPhaseCompleted, respondUpsertRequestTimedOut, } from "./upsertRequestTimeout.js";
|
|
5
|
+
const UPSERT_BODY_PHASE_STATE = Symbol("upsertBodyPhaseState");
|
|
6
|
+
function asUpsertBodyAwareRequest(req) {
|
|
7
|
+
return req;
|
|
8
|
+
}
|
|
9
|
+
function getUpsertBodyPhaseState(req) {
|
|
10
|
+
return asUpsertBodyAwareRequest(req)[UPSERT_BODY_PHASE_STATE];
|
|
11
|
+
}
|
|
12
|
+
function getOrCreateUpsertBodyPhaseState(req) {
|
|
13
|
+
const request = asUpsertBodyAwareRequest(req);
|
|
14
|
+
request[UPSERT_BODY_PHASE_STATE] ??= {
|
|
15
|
+
observedBodyBytes: 0,
|
|
16
|
+
bodyParseCompleted: false,
|
|
17
|
+
bodyPhaseLogged: false,
|
|
18
|
+
};
|
|
19
|
+
return request[UPSERT_BODY_PHASE_STATE];
|
|
20
|
+
}
|
|
21
|
+
function appendObservedBodyBytes(state, chunk) {
|
|
22
|
+
if (typeof chunk === "string") {
|
|
23
|
+
state.observedBodyBytes += Buffer.byteLength(chunk);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (chunk instanceof Uint8Array) {
|
|
27
|
+
state.observedBodyBytes += chunk.byteLength;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function parseContentLength(req) {
|
|
31
|
+
const raw = req.header("content-length");
|
|
32
|
+
if (!raw) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const parsed = Number(raw);
|
|
36
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
37
|
+
}
|
|
38
|
+
function buildUpsertBodyPhaseLogFields(req, state) {
|
|
39
|
+
const timeToBodyParsedMs = elapsedMsFromRequestStart(state.bodyParsedAtNs);
|
|
40
|
+
const timeToBodyBufferedMs = elapsedMsFromRequestStart(state.bodyBufferedAtNs);
|
|
41
|
+
const timeToFirstChunkMs = elapsedMsFromRequestStart(state.firstChunkAtNs);
|
|
42
|
+
const bodyReadMs = state.firstChunkAtNs !== undefined && state.bodyBufferedAtNs !== undefined
|
|
43
|
+
? elapsedMsBetween(state.firstChunkAtNs, state.bodyBufferedAtNs)
|
|
44
|
+
: undefined;
|
|
45
|
+
const jsonParseMs = state.bodyBufferedAtNs !== undefined && state.bodyParsedAtNs !== undefined
|
|
46
|
+
? elapsedMsBetween(state.bodyBufferedAtNs, state.bodyParsedAtNs)
|
|
47
|
+
: undefined;
|
|
48
|
+
return {
|
|
49
|
+
phase: "upsertBody",
|
|
50
|
+
timeToFirstChunkMs,
|
|
51
|
+
timeToBodyBufferedMs,
|
|
52
|
+
timeToBodyParsedMs,
|
|
53
|
+
bodyReadMs,
|
|
54
|
+
jsonParseMs,
|
|
55
|
+
observedBodyBytes: state.observedBodyBytes,
|
|
56
|
+
parsedBodyBytes: state.parsedBodyBytes,
|
|
57
|
+
bodyParseCompleted: state.bodyParseCompleted,
|
|
58
|
+
contentLength: parseContentLength(req),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function didBodyBudgetExpire(state) {
|
|
62
|
+
if (UPSERT_BODY_TIMEOUT_MS <= 0 || state.bodyParsedAtNs === undefined) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const requestStartNs = getRequestStartNs();
|
|
66
|
+
if (requestStartNs === undefined) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return (state.bodyParsedAtNs - requestStartNs >=
|
|
70
|
+
BigInt(UPSERT_BODY_TIMEOUT_MS) * 1000000n);
|
|
71
|
+
}
|
|
72
|
+
function extractBodyErrorType(err) {
|
|
73
|
+
if (!err || typeof err !== "object") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return "type" in err && typeof err.type === "string" ? err.type : undefined;
|
|
77
|
+
}
|
|
78
|
+
function extractBodyErrorStatus(err) {
|
|
79
|
+
if (!err || typeof err !== "object") {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if ("statusCode" in err && typeof err.statusCode === "number") {
|
|
83
|
+
return err.statusCode;
|
|
84
|
+
}
|
|
85
|
+
return "status" in err && typeof err.status === "number"
|
|
86
|
+
? err.status
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
89
|
+
export function instrumentUpsertRequestBody(req, _res, next) {
|
|
90
|
+
if (!isUpsertRequestTarget(req)) {
|
|
91
|
+
next();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const state = getOrCreateUpsertBodyPhaseState(req);
|
|
95
|
+
// This must stay immediately before express.json() and call next()
|
|
96
|
+
// synchronously: attaching a "data" listener switches the request stream
|
|
97
|
+
// into flowing mode before body-parser attaches its own listeners.
|
|
98
|
+
req.on("data", (chunk) => {
|
|
99
|
+
if (state.firstChunkAtNs === undefined) {
|
|
100
|
+
state.firstChunkAtNs = getMonotonicTimeNs();
|
|
101
|
+
}
|
|
102
|
+
appendObservedBodyBytes(state, chunk);
|
|
103
|
+
});
|
|
104
|
+
next();
|
|
105
|
+
}
|
|
106
|
+
export function verifyUpsertRequestBody(req, _res, buf) {
|
|
107
|
+
if (!isUpsertRequestTarget(req)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const state = getOrCreateUpsertBodyPhaseState(req);
|
|
111
|
+
state.bodyBufferedAtNs = getMonotonicTimeNs();
|
|
112
|
+
state.parsedBodyBytes = buf.byteLength;
|
|
113
|
+
markUpsertBodyPhaseCompleted(req);
|
|
114
|
+
}
|
|
115
|
+
export function logUpsertBodyPhase(req, res, next) {
|
|
116
|
+
if (!isUpsertRequestTarget(req)) {
|
|
117
|
+
next();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const state = getUpsertBodyPhaseState(req);
|
|
121
|
+
if (!state || state.bodyPhaseLogged) {
|
|
122
|
+
next();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (isUpsertRequestTimedOut(req)) {
|
|
126
|
+
// Timeout response already went out. Preserve parse timing for the
|
|
127
|
+
// timeout log path, but suppress the normal success log and stop here.
|
|
128
|
+
state.bodyParsedAtNs = getMonotonicTimeNs();
|
|
129
|
+
state.bodyParseCompleted = true;
|
|
130
|
+
markUpsertBodyPhaseCompleted(req);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
state.bodyParsedAtNs = getMonotonicTimeNs();
|
|
134
|
+
state.bodyParseCompleted = true;
|
|
135
|
+
if (didBodyBudgetExpire(state)) {
|
|
136
|
+
respondUpsertRequestTimedOut({
|
|
137
|
+
req,
|
|
138
|
+
res,
|
|
139
|
+
timeoutMs: UPSERT_BODY_TIMEOUT_MS,
|
|
140
|
+
timeoutPhase: "parse",
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
state.bodyPhaseLogged = true;
|
|
145
|
+
logger.info(buildUpsertBodyPhaseLogFields(req, state), "upsert: body phase");
|
|
146
|
+
next();
|
|
147
|
+
}
|
|
148
|
+
export function logUpsertBodyPhaseError(err, req, _res, next) {
|
|
149
|
+
if (!isUpsertRequestTarget(req)) {
|
|
150
|
+
next(err);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const state = getUpsertBodyPhaseState(req);
|
|
154
|
+
if (!state || state.bodyPhaseLogged) {
|
|
155
|
+
next(err);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isUpsertRequestTimedOut(req)) {
|
|
159
|
+
markUpsertBodyPhaseCompleted(req);
|
|
160
|
+
next(err);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
state.bodyPhaseLogged = true;
|
|
164
|
+
markUpsertBodyPhaseCompleted(req);
|
|
165
|
+
logger.warn({
|
|
166
|
+
...buildUpsertBodyPhaseLogFields(req, state),
|
|
167
|
+
bodyErrorType: extractBodyErrorType(err),
|
|
168
|
+
bodyErrorStatus: extractBodyErrorStatus(err),
|
|
169
|
+
}, "upsert: body phase failed");
|
|
170
|
+
next(err);
|
|
171
|
+
}
|
|
172
|
+
export function getUpsertBodyPhaseTimeoutLogFields(req) {
|
|
173
|
+
if (!isUpsertRequestTarget(req)) {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
const state = getUpsertBodyPhaseState(req);
|
|
177
|
+
if (!state) {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
...buildUpsertBodyPhaseLogFields(req, state),
|
|
182
|
+
bodyPhaseLogged: state.bodyPhaseLogged,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
export type UpsertTimeoutPhase = "body" | "parse" | "processing";
|
|
3
|
+
export declare function isUpsertRequestTarget(req: Request): boolean;
|
|
4
|
+
export declare function markUpsertRequestTimedOut(req: Request, timeoutMs: number, timeoutPhase: UpsertTimeoutPhase): void;
|
|
5
|
+
export declare function isUpsertRequestTimedOut(req: Request): boolean;
|
|
6
|
+
export declare function getUpsertRequestTimeoutMs(req: Request): number | undefined;
|
|
7
|
+
export declare function getUpsertRequestTimeoutPhase(req: Request): UpsertTimeoutPhase | undefined;
|
|
8
|
+
export declare function respondUpsertRequestTimedOut(args: {
|
|
9
|
+
req: Request;
|
|
10
|
+
res: Response;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
timeoutPhase: UpsertTimeoutPhase;
|
|
13
|
+
}): boolean;
|
|
14
|
+
export declare function markUpsertBodyPhaseCompleted(req: Request): void;
|
|
15
|
+
export declare function upsertBodyTimeout(req: Request, res: Response, next: NextFunction): void;
|
|
16
|
+
export declare function upsertProcessingTimeout(req: Request, res: Response, next: NextFunction): void;
|