@neat.is/core 0.4.3 → 0.4.4
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/dist/{chunk-NTQHMXWE.js → chunk-G3YGPWJL.js} +2 -2
- package/dist/{chunk-CB2UK4EH.js → chunk-J4YBTD24.js} +44 -3
- package/dist/{chunk-CB2UK4EH.js.map → chunk-J4YBTD24.js.map} +1 -1
- package/dist/{chunk-D5PIJFBE.js → chunk-YJOA7BBF.js} +2 -2
- package/dist/{chunk-KYRIQIPG.js → chunk-ZVNP3ZDH.js} +97 -25
- package/dist/chunk-ZVNP3ZDH.js.map +1 -0
- package/dist/cli.cjs +276 -99
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +184 -79
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +136 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +136 -23
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-QTX2YQJZ.js → otel-grpc-FIERFRGD.js} +3 -3
- package/dist/server.cjs +95 -23
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-KYRIQIPG.js.map +0 -1
- /package/dist/{chunk-NTQHMXWE.js.map → chunk-G3YGPWJL.js.map} +0 -0
- /package/dist/{chunk-D5PIJFBE.js.map → chunk-YJOA7BBF.js.map} +0 -0
- /package/dist/{otel-grpc-QTX2YQJZ.js.map → otel-grpc-FIERFRGD.js.map} +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -153,9 +153,12 @@ interface ParsedSpan {
|
|
|
153
153
|
}
|
|
154
154
|
type AttributeValue = string | number | boolean | bigint | string[] | number[] | boolean[] | null;
|
|
155
155
|
type SpanHandler = (span: ParsedSpan) => void | Promise<void>;
|
|
156
|
+
type ProjectSpanHandler = (project: string, span: ParsedSpan) => void | Promise<void>;
|
|
156
157
|
interface BuildOtelReceiverOptions {
|
|
157
158
|
onSpan: SpanHandler;
|
|
158
159
|
onErrorSpanSync?: (span: ParsedSpan) => Promise<void>;
|
|
160
|
+
onProjectSpan?: ProjectSpanHandler;
|
|
161
|
+
onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>;
|
|
159
162
|
bodyLimit?: number;
|
|
160
163
|
authToken?: string;
|
|
161
164
|
trustProxy?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -153,9 +153,12 @@ interface ParsedSpan {
|
|
|
153
153
|
}
|
|
154
154
|
type AttributeValue = string | number | boolean | bigint | string[] | number[] | boolean[] | null;
|
|
155
155
|
type SpanHandler = (span: ParsedSpan) => void | Promise<void>;
|
|
156
|
+
type ProjectSpanHandler = (project: string, span: ParsedSpan) => void | Promise<void>;
|
|
156
157
|
interface BuildOtelReceiverOptions {
|
|
157
158
|
onSpan: SpanHandler;
|
|
158
159
|
onErrorSpanSync?: (span: ParsedSpan) => Promise<void>;
|
|
160
|
+
onProjectSpan?: ProjectSpanHandler;
|
|
161
|
+
onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>;
|
|
159
162
|
bodyLimit?: number;
|
|
160
163
|
authToken?: string;
|
|
161
164
|
trustProxy?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-J4YBTD24.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,15 +37,15 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-G3YGPWJL.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-YJOA7BBF.js";
|
|
44
44
|
import {
|
|
45
45
|
buildOtelReceiver,
|
|
46
46
|
logSpanHandler,
|
|
47
47
|
parseOtlpRequest
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-ZVNP3ZDH.js";
|
|
49
49
|
export {
|
|
50
50
|
ProjectNameCollisionError,
|
|
51
51
|
addProject,
|
package/dist/neatd.cjs
CHANGED
|
@@ -437,6 +437,64 @@ async function buildOtelReceiver(opts) {
|
|
|
437
437
|
for (const s of spans) queue.push(s);
|
|
438
438
|
drainPromise = drainPromise.then(() => drain());
|
|
439
439
|
};
|
|
440
|
+
const projectQueue = [];
|
|
441
|
+
let projectDraining = false;
|
|
442
|
+
let projectDrainPromise = Promise.resolve();
|
|
443
|
+
const drainProject = async () => {
|
|
444
|
+
if (projectDraining) return;
|
|
445
|
+
projectDraining = true;
|
|
446
|
+
try {
|
|
447
|
+
while (projectQueue.length > 0) {
|
|
448
|
+
const { project, span } = projectQueue.shift();
|
|
449
|
+
try {
|
|
450
|
+
if (opts.onProjectSpan) {
|
|
451
|
+
await opts.onProjectSpan(project, span);
|
|
452
|
+
} else {
|
|
453
|
+
await opts.onSpan(span);
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.warn(`[neat] otel handler error: ${err.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} finally {
|
|
460
|
+
projectDraining = false;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
const enqueueProject = (project, spans) => {
|
|
464
|
+
if (spans.length === 0) return;
|
|
465
|
+
for (const s of spans) projectQueue.push({ project, span: s });
|
|
466
|
+
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
467
|
+
};
|
|
468
|
+
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
469
|
+
function warnLegacyEndpoint(serviceName) {
|
|
470
|
+
if (legacyEndpointWarned.has(serviceName)) return;
|
|
471
|
+
legacyEndpointWarned.add(serviceName);
|
|
472
|
+
console.warn(
|
|
473
|
+
`[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name="${serviceName}").`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
async function readOtlpBody(req2) {
|
|
477
|
+
const ct = (req2.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
478
|
+
if (ct === "application/x-protobuf") {
|
|
479
|
+
try {
|
|
480
|
+
const body = await decodeProtobufBody(req2.body);
|
|
481
|
+
return { ok: true, body, flavor: "protobuf" };
|
|
482
|
+
} catch (err) {
|
|
483
|
+
return { ok: false, code: 400, error: `protobuf decode failed: ${err.message}` };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!ct || ct === "application/json") {
|
|
487
|
+
return { ok: true, body: req2.body ?? {}, flavor: "json" };
|
|
488
|
+
}
|
|
489
|
+
return { ok: false, code: 415, error: `unsupported content-type: ${ct}` };
|
|
490
|
+
}
|
|
491
|
+
function sendOtlpSuccess(reply, flavor) {
|
|
492
|
+
if (flavor === "protobuf") {
|
|
493
|
+
const buf = encodeProtobufResponseBody();
|
|
494
|
+
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
495
|
+
}
|
|
496
|
+
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
497
|
+
}
|
|
440
498
|
app.addContentTypeParser(
|
|
441
499
|
"application/x-protobuf",
|
|
442
500
|
{ parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
|
|
@@ -446,26 +504,44 @@ async function buildOtelReceiver(opts) {
|
|
|
446
504
|
);
|
|
447
505
|
app.get("/health", async () => ({ ok: true }));
|
|
448
506
|
app.post("/v1/traces", async (req2, reply) => {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
507
|
+
const result = await readOtlpBody(req2);
|
|
508
|
+
if (!result.ok) {
|
|
509
|
+
return reply.code(result.code).send({ error: result.error });
|
|
510
|
+
}
|
|
511
|
+
const spans = parseOtlpRequest(result.body);
|
|
512
|
+
for (const s of spans) warnLegacyEndpoint(s.service);
|
|
513
|
+
if (opts.onErrorSpanSync) {
|
|
454
514
|
try {
|
|
455
|
-
|
|
515
|
+
for (const span of spans) {
|
|
516
|
+
if (span.statusCode === 2) await opts.onErrorSpanSync(span);
|
|
517
|
+
}
|
|
456
518
|
} catch (err) {
|
|
457
|
-
return reply.code(
|
|
458
|
-
error: `
|
|
519
|
+
return reply.code(500).send({
|
|
520
|
+
error: `error-event write failed: ${err.message}`
|
|
459
521
|
});
|
|
460
522
|
}
|
|
461
|
-
} else if (!ct || ct === "application/json") {
|
|
462
|
-
responseFlavor = "json";
|
|
463
|
-
body = req2.body ?? {};
|
|
464
|
-
} else {
|
|
465
|
-
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
466
523
|
}
|
|
467
|
-
|
|
468
|
-
|
|
524
|
+
enqueue(spans);
|
|
525
|
+
return sendOtlpSuccess(reply, result.flavor);
|
|
526
|
+
});
|
|
527
|
+
app.post("/projects/:project/v1/traces", async (req2, reply) => {
|
|
528
|
+
const project = req2.params.project;
|
|
529
|
+
const result = await readOtlpBody(req2);
|
|
530
|
+
if (!result.ok) {
|
|
531
|
+
return reply.code(result.code).send({ error: result.error });
|
|
532
|
+
}
|
|
533
|
+
const spans = parseOtlpRequest(result.body);
|
|
534
|
+
if (opts.onProjectErrorSpanSync) {
|
|
535
|
+
try {
|
|
536
|
+
for (const span of spans) {
|
|
537
|
+
if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span);
|
|
538
|
+
}
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return reply.code(500).send({
|
|
541
|
+
error: `error-event write failed: ${err.message}`
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
} else if (opts.onErrorSpanSync) {
|
|
469
545
|
try {
|
|
470
546
|
for (const span of spans) {
|
|
471
547
|
if (span.statusCode === 2) await opts.onErrorSpanSync(span);
|
|
@@ -476,17 +552,13 @@ async function buildOtelReceiver(opts) {
|
|
|
476
552
|
});
|
|
477
553
|
}
|
|
478
554
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const buf = encodeProtobufResponseBody();
|
|
482
|
-
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
483
|
-
}
|
|
484
|
-
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
555
|
+
enqueueProject(project, spans);
|
|
556
|
+
return sendOtlpSuccess(reply, result.flavor);
|
|
485
557
|
});
|
|
486
558
|
const decorated = app;
|
|
487
559
|
decorated.flushPending = async () => {
|
|
488
|
-
while (queue.length > 0 || draining) {
|
|
489
|
-
await drainPromise;
|
|
560
|
+
while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {
|
|
561
|
+
await Promise.all([drainPromise, projectDrainPromise]);
|
|
490
562
|
}
|
|
491
563
|
};
|
|
492
564
|
return decorated;
|
|
@@ -5680,6 +5752,25 @@ async function startDaemon(opts = {}) {
|
|
|
5680
5752
|
}
|
|
5681
5753
|
return slot.status === "active" ? slot : null;
|
|
5682
5754
|
}
|
|
5755
|
+
async function resolveSlotByName(project, serviceName, traceId) {
|
|
5756
|
+
const liveEntries = await listProjects().catch(() => []);
|
|
5757
|
+
let slot = slots.get(project);
|
|
5758
|
+
if (!slot) {
|
|
5759
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
5760
|
+
return null;
|
|
5761
|
+
}
|
|
5762
|
+
if (slot.status === "broken") {
|
|
5763
|
+
const entry2 = liveEntries.find((e) => e.name === slot.entry.name);
|
|
5764
|
+
if (entry2) {
|
|
5765
|
+
slot = await tryRecoverSlot(entry2);
|
|
5766
|
+
}
|
|
5767
|
+
if (slot.status !== "active") {
|
|
5768
|
+
warnDroppedSpan(slot.entry.name, slot.errorReason ?? "unknown");
|
|
5769
|
+
return null;
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
return slot.status === "active" ? slot : null;
|
|
5773
|
+
}
|
|
5683
5774
|
try {
|
|
5684
5775
|
otlpApp = await buildOtelReceiver({
|
|
5685
5776
|
authToken: auth.otelToken,
|
|
@@ -5702,6 +5793,28 @@ async function startDaemon(opts = {}) {
|
|
|
5702
5793
|
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
5703
5794
|
if (!slot) return;
|
|
5704
5795
|
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
5796
|
+
},
|
|
5797
|
+
// Project-scoped route (issue #367) — the URL already named the
|
|
5798
|
+
// project. Resolution is a direct slot lookup; service.name resolves
|
|
5799
|
+
// the ServiceNode inside the slot's graph instead of which project
|
|
5800
|
+
// owns the span.
|
|
5801
|
+
onProjectSpan: async (project, span) => {
|
|
5802
|
+
const slot = await resolveSlotByName(project, span.service, span.traceId);
|
|
5803
|
+
if (!slot) return;
|
|
5804
|
+
await handleSpan(
|
|
5805
|
+
{
|
|
5806
|
+
graph: slot.graph,
|
|
5807
|
+
errorsPath: slot.paths.errorsPath,
|
|
5808
|
+
project: slot.entry.name,
|
|
5809
|
+
writeErrorEventInline: false
|
|
5810
|
+
},
|
|
5811
|
+
span
|
|
5812
|
+
);
|
|
5813
|
+
},
|
|
5814
|
+
onProjectErrorSpanSync: async (project, span) => {
|
|
5815
|
+
const slot = await resolveSlotByName(project, span.service, span.traceId);
|
|
5816
|
+
if (!slot) return;
|
|
5817
|
+
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
5705
5818
|
}
|
|
5706
5819
|
});
|
|
5707
5820
|
otlpAddress = await otlpApp.listen({ port: otlpPort, host });
|