@neat.is/core 0.4.3 → 0.4.5
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-D5PIJFBE.js → chunk-3QCRUEQD.js} +2 -2
- package/dist/{chunk-CB2UK4EH.js → chunk-6GXBAR3M.js} +44 -3
- package/dist/{chunk-CB2UK4EH.js.map → chunk-6GXBAR3M.js.map} +1 -1
- package/dist/{chunk-KYRIQIPG.js → chunk-HVF4S7J3.js} +101 -26
- package/dist/chunk-HVF4S7J3.js.map +1 -0
- package/dist/{chunk-NTQHMXWE.js → chunk-RBWL4HRB.js} +13 -2
- package/dist/chunk-RBWL4HRB.js.map +1 -0
- package/dist/cli.cjs +430 -124
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +323 -103
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +151 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +151 -24
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-QTX2YQJZ.js → otel-grpc-VSPMP3OS.js} +3 -3
- package/dist/server.cjs +110 -24
- 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 +0 -1
- /package/dist/{chunk-D5PIJFBE.js.map → chunk-3QCRUEQD.js.map} +0 -0
- /package/dist/{otel-grpc-QTX2YQJZ.js.map → otel-grpc-VSPMP3OS.js.map} +0 -0
package/dist/neatd.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6GXBAR3M.js";
|
|
5
5
|
import {
|
|
6
6
|
listProjects,
|
|
7
7
|
registryPath
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RBWL4HRB.js";
|
|
9
9
|
import {
|
|
10
10
|
BindAuthorityError,
|
|
11
11
|
__require
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HVF4S7J3.js";
|
|
13
13
|
|
|
14
14
|
// src/neatd.ts
|
|
15
15
|
import { promises as fs } from "fs";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
reshapeGrpcRequest,
|
|
3
3
|
startOtelGrpcReceiver
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-3QCRUEQD.js";
|
|
5
|
+
import "./chunk-HVF4S7J3.js";
|
|
6
6
|
export {
|
|
7
7
|
reshapeGrpcRequest,
|
|
8
8
|
startOtelGrpcReceiver
|
|
9
9
|
};
|
|
10
|
-
//# sourceMappingURL=otel-grpc-
|
|
10
|
+
//# sourceMappingURL=otel-grpc-VSPMP3OS.js.map
|
package/dist/server.cjs
CHANGED
|
@@ -336,12 +336,15 @@ function parseOtlpRequest(body) {
|
|
|
336
336
|
const out = [];
|
|
337
337
|
for (const rs of body.resourceSpans ?? []) {
|
|
338
338
|
const resourceAttrs = attrsToRecord(rs.resource?.attributes);
|
|
339
|
-
const
|
|
339
|
+
const rawServiceName = resourceAttrs["service.name"];
|
|
340
|
+
const resourceServiceNamePresent = typeof rawServiceName === "string" && rawServiceName.length > 0;
|
|
341
|
+
const service = resourceServiceNamePresent ? rawServiceName : "unidentified";
|
|
340
342
|
for (const ss of rs.scopeSpans ?? []) {
|
|
341
343
|
for (const span of ss.spans ?? []) {
|
|
342
344
|
const attrs = attrsToRecord(span.attributes);
|
|
343
345
|
const parsed = {
|
|
344
346
|
service,
|
|
347
|
+
resourceServiceNamePresent,
|
|
345
348
|
traceId: span.traceId ?? "",
|
|
346
349
|
spanId: span.spanId ?? "",
|
|
347
350
|
parentSpanId: span.parentSpanId || void 0,
|
|
@@ -436,6 +439,64 @@ async function buildOtelReceiver(opts) {
|
|
|
436
439
|
for (const s of spans) queue.push(s);
|
|
437
440
|
drainPromise = drainPromise.then(() => drain());
|
|
438
441
|
};
|
|
442
|
+
const projectQueue = [];
|
|
443
|
+
let projectDraining = false;
|
|
444
|
+
let projectDrainPromise = Promise.resolve();
|
|
445
|
+
const drainProject = async () => {
|
|
446
|
+
if (projectDraining) return;
|
|
447
|
+
projectDraining = true;
|
|
448
|
+
try {
|
|
449
|
+
while (projectQueue.length > 0) {
|
|
450
|
+
const { project, span } = projectQueue.shift();
|
|
451
|
+
try {
|
|
452
|
+
if (opts.onProjectSpan) {
|
|
453
|
+
await opts.onProjectSpan(project, span);
|
|
454
|
+
} else {
|
|
455
|
+
await opts.onSpan(span);
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.warn(`[neat] otel handler error: ${err.message}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
projectDraining = false;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const enqueueProject = (project, spans) => {
|
|
466
|
+
if (spans.length === 0) return;
|
|
467
|
+
for (const s of spans) projectQueue.push({ project, span: s });
|
|
468
|
+
projectDrainPromise = projectDrainPromise.then(() => drainProject());
|
|
469
|
+
};
|
|
470
|
+
const legacyEndpointWarned = /* @__PURE__ */ new Set();
|
|
471
|
+
function warnLegacyEndpoint(serviceName) {
|
|
472
|
+
if (legacyEndpointWarned.has(serviceName)) return;
|
|
473
|
+
legacyEndpointWarned.add(serviceName);
|
|
474
|
+
console.warn(
|
|
475
|
+
`[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name="${serviceName}").`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
async function readOtlpBody(req) {
|
|
479
|
+
const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
480
|
+
if (ct === "application/x-protobuf") {
|
|
481
|
+
try {
|
|
482
|
+
const body = await decodeProtobufBody(req.body);
|
|
483
|
+
return { ok: true, body, flavor: "protobuf" };
|
|
484
|
+
} catch (err) {
|
|
485
|
+
return { ok: false, code: 400, error: `protobuf decode failed: ${err.message}` };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!ct || ct === "application/json") {
|
|
489
|
+
return { ok: true, body: req.body ?? {}, flavor: "json" };
|
|
490
|
+
}
|
|
491
|
+
return { ok: false, code: 415, error: `unsupported content-type: ${ct}` };
|
|
492
|
+
}
|
|
493
|
+
function sendOtlpSuccess(reply, flavor) {
|
|
494
|
+
if (flavor === "protobuf") {
|
|
495
|
+
const buf = encodeProtobufResponseBody();
|
|
496
|
+
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
497
|
+
}
|
|
498
|
+
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
499
|
+
}
|
|
439
500
|
app.addContentTypeParser(
|
|
440
501
|
"application/x-protobuf",
|
|
441
502
|
{ parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
|
|
@@ -445,26 +506,44 @@ async function buildOtelReceiver(opts) {
|
|
|
445
506
|
);
|
|
446
507
|
app.get("/health", async () => ({ ok: true }));
|
|
447
508
|
app.post("/v1/traces", async (req, reply) => {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
509
|
+
const result = await readOtlpBody(req);
|
|
510
|
+
if (!result.ok) {
|
|
511
|
+
return reply.code(result.code).send({ error: result.error });
|
|
512
|
+
}
|
|
513
|
+
const spans = parseOtlpRequest(result.body);
|
|
514
|
+
for (const s of spans) warnLegacyEndpoint(s.service);
|
|
515
|
+
if (opts.onErrorSpanSync) {
|
|
453
516
|
try {
|
|
454
|
-
|
|
517
|
+
for (const span of spans) {
|
|
518
|
+
if (span.statusCode === 2) await opts.onErrorSpanSync(span);
|
|
519
|
+
}
|
|
455
520
|
} catch (err) {
|
|
456
|
-
return reply.code(
|
|
457
|
-
error: `
|
|
521
|
+
return reply.code(500).send({
|
|
522
|
+
error: `error-event write failed: ${err.message}`
|
|
458
523
|
});
|
|
459
524
|
}
|
|
460
|
-
} else if (!ct || ct === "application/json") {
|
|
461
|
-
responseFlavor = "json";
|
|
462
|
-
body = req.body ?? {};
|
|
463
|
-
} else {
|
|
464
|
-
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
465
525
|
}
|
|
466
|
-
|
|
467
|
-
|
|
526
|
+
enqueue(spans);
|
|
527
|
+
return sendOtlpSuccess(reply, result.flavor);
|
|
528
|
+
});
|
|
529
|
+
app.post("/projects/:project/v1/traces", async (req, reply) => {
|
|
530
|
+
const project = req.params.project;
|
|
531
|
+
const result = await readOtlpBody(req);
|
|
532
|
+
if (!result.ok) {
|
|
533
|
+
return reply.code(result.code).send({ error: result.error });
|
|
534
|
+
}
|
|
535
|
+
const spans = parseOtlpRequest(result.body);
|
|
536
|
+
if (opts.onProjectErrorSpanSync) {
|
|
537
|
+
try {
|
|
538
|
+
for (const span of spans) {
|
|
539
|
+
if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span);
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
return reply.code(500).send({
|
|
543
|
+
error: `error-event write failed: ${err.message}`
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} else if (opts.onErrorSpanSync) {
|
|
468
547
|
try {
|
|
469
548
|
for (const span of spans) {
|
|
470
549
|
if (span.statusCode === 2) await opts.onErrorSpanSync(span);
|
|
@@ -475,17 +554,13 @@ async function buildOtelReceiver(opts) {
|
|
|
475
554
|
});
|
|
476
555
|
}
|
|
477
556
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const buf = encodeProtobufResponseBody();
|
|
481
|
-
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
482
|
-
}
|
|
483
|
-
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
557
|
+
enqueueProject(project, spans);
|
|
558
|
+
return sendOtlpSuccess(reply, result.flavor);
|
|
484
559
|
});
|
|
485
560
|
const decorated = app;
|
|
486
561
|
decorated.flushPending = async () => {
|
|
487
|
-
while (queue.length > 0 || draining) {
|
|
488
|
-
await drainPromise;
|
|
562
|
+
while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {
|
|
563
|
+
await Promise.all([drainPromise, projectDrainPromise]);
|
|
489
564
|
}
|
|
490
565
|
};
|
|
491
566
|
return decorated;
|
|
@@ -1756,6 +1831,14 @@ function thresholdForEdgeType(edgeType, overrides) {
|
|
|
1756
1831
|
function nowIso(ctx) {
|
|
1757
1832
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1758
1833
|
}
|
|
1834
|
+
var unidentifiedWarnedProjects = /* @__PURE__ */ new Set();
|
|
1835
|
+
function warnUnidentifiedSpan(project) {
|
|
1836
|
+
if (unidentifiedWarnedProjects.has(project)) return;
|
|
1837
|
+
unidentifiedWarnedProjects.add(project);
|
|
1838
|
+
console.warn(
|
|
1839
|
+
`[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1759
1842
|
function pickAttr(span, ...keys) {
|
|
1760
1843
|
for (const k of keys) {
|
|
1761
1844
|
const v = span.attributes[k];
|
|
@@ -1985,6 +2068,9 @@ async function handleSpan(ctx, span) {
|
|
|
1985
2068
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1986
2069
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1987
2070
|
const env = span.env ?? "unknown";
|
|
2071
|
+
if (span.resourceServiceNamePresent === false) {
|
|
2072
|
+
warnUnidentifiedSpan(ctx.project ?? DEFAULT_PROJECT);
|
|
2073
|
+
}
|
|
1988
2074
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1989
2075
|
const isError = span.statusCode === 2;
|
|
1990
2076
|
cacheSpanService(span, nowMs);
|