@neat.is/core 0.3.6 → 0.3.7
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-4ASCXBZF.js → chunk-75IBA4UL.js} +34 -5
- package/dist/chunk-75IBA4UL.js.map +1 -0
- package/dist/{chunk-G3PDTGOW.js → chunk-CY67YKNO.js} +2 -2
- package/dist/{chunk-YHQYHFI3.js → chunk-EDHOCFOG.js} +86 -14
- package/dist/chunk-EDHOCFOG.js.map +1 -0
- package/dist/cli.cjs +93 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +62 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +117 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +3 -3
- package/dist/neatd.cjs +117 -16
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/{otel-grpc-J4O2SIBZ.js → otel-grpc-PM4SWPZE.js} +3 -3
- package/dist/server.cjs +33 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-4ASCXBZF.js.map +0 -1
- package/dist/chunk-YHQYHFI3.js.map +0 -1
- /package/dist/{chunk-G3PDTGOW.js.map → chunk-CY67YKNO.js.map} +0 -0
- /package/dist/{otel-grpc-J4O2SIBZ.js.map → otel-grpc-PM4SWPZE.js.map} +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -358,6 +358,29 @@ interface DaemonHandle {
|
|
|
358
358
|
*
|
|
359
359
|
* Pure function. Daemon callers pass a snapshot of the registry to avoid
|
|
360
360
|
* per-span fs reads.
|
|
361
|
+
*
|
|
362
|
+
* Matching order (ADR-072 — real-world `service.name` rarely equals project
|
|
363
|
+
* name; monorepos publish per-package names like `brief-api` under a
|
|
364
|
+
* project named `brief`):
|
|
365
|
+
*
|
|
366
|
+
* 1. Exact: `entry.name === serviceName`.
|
|
367
|
+
* 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token
|
|
368
|
+
* of `serviceName` (`brief` matches `brief-api`, `brief_worker`).
|
|
369
|
+
* Longest-match wins so `brief-api` beats `brief` when both are
|
|
370
|
+
* registered.
|
|
371
|
+
* 3. Containment as a separator-delimited token (`api` inside
|
|
372
|
+
* `brief-api-staging`).
|
|
373
|
+
*
|
|
374
|
+
* Routing eligibility (ADR-071):
|
|
375
|
+
* - `active` matches at every pass (the steady-state path).
|
|
376
|
+
* - `broken` also matches — the daemon needs the span to reach the broken
|
|
377
|
+
* slot so the ingest-time auto-recover path can attempt a bootstrap and
|
|
378
|
+
* lift the project back to `active`. The router only chooses the target;
|
|
379
|
+
* whether the span actually lands is the ingest handler's decision.
|
|
380
|
+
* - `paused` is intentionally not routed; the operator paused it on
|
|
381
|
+
* purpose, so the span falls through to the default-project flow.
|
|
382
|
+
*
|
|
383
|
+
* Falls back to `DEFAULT_PROJECT` when nothing matches.
|
|
361
384
|
*/
|
|
362
385
|
declare function routeSpanToProject(serviceName: string | undefined, projects: ReadonlyArray<RegistryEntry>): string;
|
|
363
386
|
declare function startDaemon(opts?: DaemonOptions): Promise<DaemonHandle>;
|
package/dist/index.d.ts
CHANGED
|
@@ -358,6 +358,29 @@ interface DaemonHandle {
|
|
|
358
358
|
*
|
|
359
359
|
* Pure function. Daemon callers pass a snapshot of the registry to avoid
|
|
360
360
|
* per-span fs reads.
|
|
361
|
+
*
|
|
362
|
+
* Matching order (ADR-072 — real-world `service.name` rarely equals project
|
|
363
|
+
* name; monorepos publish per-package names like `brief-api` under a
|
|
364
|
+
* project named `brief`):
|
|
365
|
+
*
|
|
366
|
+
* 1. Exact: `entry.name === serviceName`.
|
|
367
|
+
* 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token
|
|
368
|
+
* of `serviceName` (`brief` matches `brief-api`, `brief_worker`).
|
|
369
|
+
* Longest-match wins so `brief-api` beats `brief` when both are
|
|
370
|
+
* registered.
|
|
371
|
+
* 3. Containment as a separator-delimited token (`api` inside
|
|
372
|
+
* `brief-api-staging`).
|
|
373
|
+
*
|
|
374
|
+
* Routing eligibility (ADR-071):
|
|
375
|
+
* - `active` matches at every pass (the steady-state path).
|
|
376
|
+
* - `broken` also matches — the daemon needs the span to reach the broken
|
|
377
|
+
* slot so the ingest-time auto-recover path can attempt a bootstrap and
|
|
378
|
+
* lift the project back to `active`. The router only chooses the target;
|
|
379
|
+
* whether the span actually lands is the ingest handler's decision.
|
|
380
|
+
* - `paused` is intentionally not routed; the operator paused it on
|
|
381
|
+
* purpose, so the span falls through to the default-project flow.
|
|
382
|
+
*
|
|
383
|
+
* Falls back to `DEFAULT_PROJECT` when nothing matches.
|
|
361
384
|
*/
|
|
362
385
|
declare function routeSpanToProject(serviceName: string | undefined, projects: ReadonlyArray<RegistryEntry>): string;
|
|
363
386
|
declare function startDaemon(opts?: DaemonOptions): Promise<DaemonHandle>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-EDHOCFOG.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -40,12 +40,12 @@ import {
|
|
|
40
40
|
} from "./chunk-ZU2RQRCN.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-CY67YKNO.js";
|
|
44
44
|
import {
|
|
45
45
|
buildOtelReceiver,
|
|
46
46
|
logSpanHandler,
|
|
47
47
|
parseOtlpRequest
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-75IBA4UL.js";
|
|
49
49
|
export {
|
|
50
50
|
ProjectNameCollisionError,
|
|
51
51
|
addProject,
|
package/dist/neatd.cjs
CHANGED
|
@@ -257,8 +257,7 @@ function parseOtlpRequest(body) {
|
|
|
257
257
|
}
|
|
258
258
|
return out;
|
|
259
259
|
}
|
|
260
|
-
function
|
|
261
|
-
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
260
|
+
function loadProtoRoot() {
|
|
262
261
|
const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
262
|
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
264
263
|
const root = new import_protobufjs.default.Root();
|
|
@@ -267,11 +266,32 @@ function loadProtobufDecoder() {
|
|
|
267
266
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
267
|
{ keepCase: true }
|
|
269
268
|
);
|
|
269
|
+
return root;
|
|
270
|
+
}
|
|
271
|
+
function loadProtobufDecoder() {
|
|
272
|
+
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
273
|
+
const root = loadProtoRoot();
|
|
270
274
|
exportTraceServiceRequestType = root.lookupType(
|
|
271
275
|
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
|
|
272
276
|
);
|
|
273
277
|
return exportTraceServiceRequestType;
|
|
274
278
|
}
|
|
279
|
+
function loadProtobufResponseEncoder() {
|
|
280
|
+
if (exportTraceServiceResponseType) return exportTraceServiceResponseType;
|
|
281
|
+
const root = loadProtoRoot();
|
|
282
|
+
exportTraceServiceResponseType = root.lookupType(
|
|
283
|
+
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse"
|
|
284
|
+
);
|
|
285
|
+
return exportTraceServiceResponseType;
|
|
286
|
+
}
|
|
287
|
+
function encodeProtobufResponseBody() {
|
|
288
|
+
if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
|
|
289
|
+
const Type = loadProtobufResponseEncoder();
|
|
290
|
+
const msg = Type.create({});
|
|
291
|
+
const encoded = Type.encode(msg).finish();
|
|
292
|
+
cachedProtobufResponseBody = Buffer.from(encoded);
|
|
293
|
+
return cachedProtobufResponseBody;
|
|
294
|
+
}
|
|
275
295
|
async function decodeProtobufBody(buf) {
|
|
276
296
|
const Type = loadProtobufDecoder();
|
|
277
297
|
const decoded = Type.decode(buf).toJSON();
|
|
@@ -318,7 +338,9 @@ async function buildOtelReceiver(opts) {
|
|
|
318
338
|
app.post("/v1/traces", async (req2, reply) => {
|
|
319
339
|
const ct = (req2.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
320
340
|
let body;
|
|
341
|
+
let responseFlavor;
|
|
321
342
|
if (ct === "application/x-protobuf") {
|
|
343
|
+
responseFlavor = "protobuf";
|
|
322
344
|
try {
|
|
323
345
|
body = await decodeProtobufBody(req2.body);
|
|
324
346
|
} catch (err) {
|
|
@@ -327,6 +349,7 @@ async function buildOtelReceiver(opts) {
|
|
|
327
349
|
});
|
|
328
350
|
}
|
|
329
351
|
} else if (!ct || ct === "application/json") {
|
|
352
|
+
responseFlavor = "json";
|
|
330
353
|
body = req2.body ?? {};
|
|
331
354
|
} else {
|
|
332
355
|
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
@@ -344,7 +367,11 @@ async function buildOtelReceiver(opts) {
|
|
|
344
367
|
}
|
|
345
368
|
}
|
|
346
369
|
enqueue(spans);
|
|
347
|
-
|
|
370
|
+
if (responseFlavor === "protobuf") {
|
|
371
|
+
const buf = encodeProtobufResponseBody();
|
|
372
|
+
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
373
|
+
}
|
|
374
|
+
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
348
375
|
});
|
|
349
376
|
const decorated = app;
|
|
350
377
|
decorated.flushPending = async () => {
|
|
@@ -354,7 +381,7 @@ async function buildOtelReceiver(opts) {
|
|
|
354
381
|
};
|
|
355
382
|
return decorated;
|
|
356
383
|
}
|
|
357
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
384
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
358
385
|
var init_otel = __esm({
|
|
359
386
|
"src/otel.ts"() {
|
|
360
387
|
"use strict";
|
|
@@ -364,6 +391,8 @@ var init_otel = __esm({
|
|
|
364
391
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
365
392
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
366
393
|
exportTraceServiceRequestType = null;
|
|
394
|
+
exportTraceServiceResponseType = null;
|
|
395
|
+
cachedProtobufResponseBody = null;
|
|
367
396
|
}
|
|
368
397
|
});
|
|
369
398
|
|
|
@@ -5055,13 +5084,35 @@ function neatHomeFor(opts) {
|
|
|
5055
5084
|
function routeSpanToProject(serviceName, projects) {
|
|
5056
5085
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
5057
5086
|
for (const entry2 of projects) {
|
|
5058
|
-
if (entry2.status
|
|
5059
|
-
if (entry2.languages.length === 0) {
|
|
5060
|
-
}
|
|
5087
|
+
if (entry2.status === "paused") continue;
|
|
5061
5088
|
if (entry2.name === serviceName) return entry2.name;
|
|
5062
5089
|
}
|
|
5090
|
+
const candidates = [];
|
|
5091
|
+
for (const entry2 of projects) {
|
|
5092
|
+
if (entry2.status === "paused") continue;
|
|
5093
|
+
if (isTokenPrefix(entry2.name, serviceName)) candidates.push(entry2);
|
|
5094
|
+
}
|
|
5095
|
+
if (candidates.length > 0) {
|
|
5096
|
+
candidates.sort((a, b) => b.name.length - a.name.length);
|
|
5097
|
+
return candidates[0].name;
|
|
5098
|
+
}
|
|
5099
|
+
for (const entry2 of projects) {
|
|
5100
|
+
if (entry2.status === "paused") continue;
|
|
5101
|
+
if (isTokenContained(entry2.name, serviceName)) return entry2.name;
|
|
5102
|
+
}
|
|
5063
5103
|
return DEFAULT_PROJECT;
|
|
5064
5104
|
}
|
|
5105
|
+
function isTokenPrefix(prefix, full) {
|
|
5106
|
+
if (prefix.length >= full.length) return false;
|
|
5107
|
+
if (!full.startsWith(prefix)) return false;
|
|
5108
|
+
const sep = full.charAt(prefix.length);
|
|
5109
|
+
return sep === "-" || sep === "_";
|
|
5110
|
+
}
|
|
5111
|
+
function isTokenContained(needle, haystack) {
|
|
5112
|
+
if (!haystack.includes(needle)) return false;
|
|
5113
|
+
const tokens = haystack.split(/[-_]/);
|
|
5114
|
+
return tokens.includes(needle);
|
|
5115
|
+
}
|
|
5065
5116
|
async function bootstrapProject(entry2) {
|
|
5066
5117
|
const paths = pathsForProject(entry2.name, import_node_path36.default.join(entry2.path, "neat-out"));
|
|
5067
5118
|
try {
|
|
@@ -5141,6 +5192,17 @@ async function startDaemon(opts = {}) {
|
|
|
5141
5192
|
`);
|
|
5142
5193
|
const slots = /* @__PURE__ */ new Map();
|
|
5143
5194
|
const registry = new Projects();
|
|
5195
|
+
const DROP_WARN_INTERVAL_MS = 6e4;
|
|
5196
|
+
const lastDropWarnAt = /* @__PURE__ */ new Map();
|
|
5197
|
+
function warnDroppedSpan(project, reason) {
|
|
5198
|
+
const now = Date.now();
|
|
5199
|
+
const prev = lastDropWarnAt.get(project) ?? 0;
|
|
5200
|
+
if (now - prev < DROP_WARN_INTERVAL_MS) return;
|
|
5201
|
+
lastDropWarnAt.set(project, now);
|
|
5202
|
+
console.warn(
|
|
5203
|
+
`[neatd] dropping span for project "${project}" \u2014 project status: broken (${reason}). Run \`neatd reload\` to retry bootstrap.`
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5144
5206
|
function upsertRegistryFromSlot(slot) {
|
|
5145
5207
|
if (slot.status !== "active") return;
|
|
5146
5208
|
registry.set(slot.entry.name, {
|
|
@@ -5149,12 +5211,38 @@ async function startDaemon(opts = {}) {
|
|
|
5149
5211
|
graph: slot.graph
|
|
5150
5212
|
});
|
|
5151
5213
|
}
|
|
5214
|
+
async function tryRecoverSlot(entry2) {
|
|
5215
|
+
try {
|
|
5216
|
+
const fresh = await bootstrapProject(entry2);
|
|
5217
|
+
slots.set(entry2.name, fresh);
|
|
5218
|
+
upsertRegistryFromSlot(fresh);
|
|
5219
|
+
if (fresh.status === "active") {
|
|
5220
|
+
await setStatus(entry2.name, "active").catch(() => {
|
|
5221
|
+
});
|
|
5222
|
+
console.log(
|
|
5223
|
+
`neatd: project "${entry2.name}" recovered from broken \u2014 active`
|
|
5224
|
+
);
|
|
5225
|
+
}
|
|
5226
|
+
return fresh;
|
|
5227
|
+
} catch (err) {
|
|
5228
|
+
console.warn(
|
|
5229
|
+
`neatd: project "${entry2.name}" still broken after recovery attempt \u2014 ${err.message}`
|
|
5230
|
+
);
|
|
5231
|
+
return slots.get(entry2.name);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5152
5234
|
async function loadAll() {
|
|
5153
5235
|
const projects = await listProjects();
|
|
5154
5236
|
const seen = /* @__PURE__ */ new Set();
|
|
5155
5237
|
for (const entry2 of projects) {
|
|
5156
5238
|
seen.add(entry2.name);
|
|
5157
|
-
|
|
5239
|
+
const existing = slots.get(entry2.name);
|
|
5240
|
+
if (existing) {
|
|
5241
|
+
if (existing.status === "broken") {
|
|
5242
|
+
await tryRecoverSlot(entry2);
|
|
5243
|
+
}
|
|
5244
|
+
continue;
|
|
5245
|
+
}
|
|
5158
5246
|
try {
|
|
5159
5247
|
const slot = await bootstrapProject(entry2);
|
|
5160
5248
|
slots.set(entry2.name, slot);
|
|
@@ -5211,13 +5299,28 @@ async function startDaemon(opts = {}) {
|
|
|
5211
5299
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
5212
5300
|
);
|
|
5213
5301
|
}
|
|
5302
|
+
async function resolveTargetSlot(serviceName) {
|
|
5303
|
+
const liveEntries = await listProjects().catch(() => []);
|
|
5304
|
+
const target = routeSpanToProject(serviceName, liveEntries);
|
|
5305
|
+
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5306
|
+
if (!slot) return null;
|
|
5307
|
+
if (slot.status === "broken") {
|
|
5308
|
+
const entry2 = liveEntries.find((e) => e.name === slot.entry.name);
|
|
5309
|
+
if (entry2) {
|
|
5310
|
+
slot = await tryRecoverSlot(entry2);
|
|
5311
|
+
}
|
|
5312
|
+
if (slot.status !== "active") {
|
|
5313
|
+
warnDroppedSpan(slot.entry.name, slot.errorReason ?? "unknown");
|
|
5314
|
+
return null;
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
return slot.status === "active" ? slot : null;
|
|
5318
|
+
}
|
|
5214
5319
|
try {
|
|
5215
5320
|
otlpApp = await buildOtelReceiver({
|
|
5216
5321
|
onSpan: async (span) => {
|
|
5217
|
-
const
|
|
5218
|
-
|
|
5219
|
-
const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5220
|
-
if (!slot || slot.status !== "active") return;
|
|
5322
|
+
const slot = await resolveTargetSlot(span.service);
|
|
5323
|
+
if (!slot) return;
|
|
5221
5324
|
await handleSpan(
|
|
5222
5325
|
{
|
|
5223
5326
|
graph: slot.graph,
|
|
@@ -5230,10 +5333,8 @@ async function startDaemon(opts = {}) {
|
|
|
5230
5333
|
);
|
|
5231
5334
|
},
|
|
5232
5335
|
onErrorSpanSync: async (span) => {
|
|
5233
|
-
const
|
|
5234
|
-
|
|
5235
|
-
const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5236
|
-
if (!slot || slot.status !== "active") return;
|
|
5336
|
+
const slot = await resolveTargetSlot(span.service);
|
|
5337
|
+
if (!slot) return;
|
|
5237
5338
|
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
5238
5339
|
}
|
|
5239
5340
|
});
|