@neat.is/core 0.3.5 → 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/index.cjs CHANGED
@@ -257,8 +257,7 @@ function parseOtlpRequest(body) {
257
257
  }
258
258
  return out;
259
259
  }
260
- function loadProtobufDecoder() {
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 (req, reply) => {
319
339
  const ct = (req.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(req.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 = req.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
- return reply.code(200).send({ partialSuccess: {} });
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 () => {
@@ -362,7 +389,7 @@ function logSpanHandler(span) {
362
389
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
363
390
  );
364
391
  }
365
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
392
+ var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
366
393
  var init_otel = __esm({
367
394
  "src/otel.ts"() {
368
395
  "use strict";
@@ -372,6 +399,8 @@ var init_otel = __esm({
372
399
  import_fastify2 = __toESM(require("fastify"), 1);
373
400
  import_protobufjs = __toESM(require("protobufjs"), 1);
374
401
  exportTraceServiceRequestType = null;
402
+ exportTraceServiceResponseType = null;
403
+ cachedProtobufResponseBody = null;
375
404
  }
376
405
  });
377
406
 
@@ -1652,7 +1681,7 @@ function buildErrorEventForReceiver(span) {
1652
1681
  service: span.service,
1653
1682
  traceId: span.traceId,
1654
1683
  spanId: span.spanId,
1655
- errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1684
+ errorMessage: span.exception?.message ?? "unknown error",
1656
1685
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1657
1686
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1658
1687
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -1744,7 +1773,7 @@ async function handleSpan(ctx, span) {
1744
1773
  service: span.service,
1745
1774
  traceId: span.traceId,
1746
1775
  spanId: span.spanId,
1747
- errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1776
+ errorMessage: span.exception?.message ?? "unknown error",
1748
1777
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1749
1778
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1750
1779
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -5259,13 +5288,35 @@ function neatHomeFor(opts) {
5259
5288
  function routeSpanToProject(serviceName, projects) {
5260
5289
  if (!serviceName) return DEFAULT_PROJECT;
5261
5290
  for (const entry of projects) {
5262
- if (entry.status !== "active") continue;
5263
- if (entry.languages.length === 0) {
5264
- }
5291
+ if (entry.status === "paused") continue;
5265
5292
  if (entry.name === serviceName) return entry.name;
5266
5293
  }
5294
+ const candidates = [];
5295
+ for (const entry of projects) {
5296
+ if (entry.status === "paused") continue;
5297
+ if (isTokenPrefix(entry.name, serviceName)) candidates.push(entry);
5298
+ }
5299
+ if (candidates.length > 0) {
5300
+ candidates.sort((a, b) => b.name.length - a.name.length);
5301
+ return candidates[0].name;
5302
+ }
5303
+ for (const entry of projects) {
5304
+ if (entry.status === "paused") continue;
5305
+ if (isTokenContained(entry.name, serviceName)) return entry.name;
5306
+ }
5267
5307
  return DEFAULT_PROJECT;
5268
5308
  }
5309
+ function isTokenPrefix(prefix, full) {
5310
+ if (prefix.length >= full.length) return false;
5311
+ if (!full.startsWith(prefix)) return false;
5312
+ const sep = full.charAt(prefix.length);
5313
+ return sep === "-" || sep === "_";
5314
+ }
5315
+ function isTokenContained(needle, haystack) {
5316
+ if (!haystack.includes(needle)) return false;
5317
+ const tokens = haystack.split(/[-_]/);
5318
+ return tokens.includes(needle);
5319
+ }
5269
5320
  async function bootstrapProject(entry) {
5270
5321
  const paths = pathsForProject(entry.name, import_node_path36.default.join(entry.path, "neat-out"));
5271
5322
  try {
@@ -5345,6 +5396,17 @@ async function startDaemon(opts = {}) {
5345
5396
  `);
5346
5397
  const slots = /* @__PURE__ */ new Map();
5347
5398
  const registry = new Projects();
5399
+ const DROP_WARN_INTERVAL_MS = 6e4;
5400
+ const lastDropWarnAt = /* @__PURE__ */ new Map();
5401
+ function warnDroppedSpan(project, reason) {
5402
+ const now = Date.now();
5403
+ const prev = lastDropWarnAt.get(project) ?? 0;
5404
+ if (now - prev < DROP_WARN_INTERVAL_MS) return;
5405
+ lastDropWarnAt.set(project, now);
5406
+ console.warn(
5407
+ `[neatd] dropping span for project "${project}" \u2014 project status: broken (${reason}). Run \`neatd reload\` to retry bootstrap.`
5408
+ );
5409
+ }
5348
5410
  function upsertRegistryFromSlot(slot) {
5349
5411
  if (slot.status !== "active") return;
5350
5412
  registry.set(slot.entry.name, {
@@ -5353,12 +5415,38 @@ async function startDaemon(opts = {}) {
5353
5415
  graph: slot.graph
5354
5416
  });
5355
5417
  }
5418
+ async function tryRecoverSlot(entry) {
5419
+ try {
5420
+ const fresh = await bootstrapProject(entry);
5421
+ slots.set(entry.name, fresh);
5422
+ upsertRegistryFromSlot(fresh);
5423
+ if (fresh.status === "active") {
5424
+ await setStatus(entry.name, "active").catch(() => {
5425
+ });
5426
+ console.log(
5427
+ `neatd: project "${entry.name}" recovered from broken \u2014 active`
5428
+ );
5429
+ }
5430
+ return fresh;
5431
+ } catch (err) {
5432
+ console.warn(
5433
+ `neatd: project "${entry.name}" still broken after recovery attempt \u2014 ${err.message}`
5434
+ );
5435
+ return slots.get(entry.name);
5436
+ }
5437
+ }
5356
5438
  async function loadAll() {
5357
5439
  const projects = await listProjects();
5358
5440
  const seen = /* @__PURE__ */ new Set();
5359
5441
  for (const entry of projects) {
5360
5442
  seen.add(entry.name);
5361
- if (slots.has(entry.name)) continue;
5443
+ const existing = slots.get(entry.name);
5444
+ if (existing) {
5445
+ if (existing.status === "broken") {
5446
+ await tryRecoverSlot(entry);
5447
+ }
5448
+ continue;
5449
+ }
5362
5450
  try {
5363
5451
  const slot = await bootstrapProject(entry);
5364
5452
  slots.set(entry.name, slot);
@@ -5415,13 +5503,28 @@ async function startDaemon(opts = {}) {
5415
5503
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
5416
5504
  );
5417
5505
  }
5506
+ async function resolveTargetSlot(serviceName) {
5507
+ const liveEntries = await listProjects().catch(() => []);
5508
+ const target = routeSpanToProject(serviceName, liveEntries);
5509
+ let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
5510
+ if (!slot) return null;
5511
+ if (slot.status === "broken") {
5512
+ const entry = liveEntries.find((e) => e.name === slot.entry.name);
5513
+ if (entry) {
5514
+ slot = await tryRecoverSlot(entry);
5515
+ }
5516
+ if (slot.status !== "active") {
5517
+ warnDroppedSpan(slot.entry.name, slot.errorReason ?? "unknown");
5518
+ return null;
5519
+ }
5520
+ }
5521
+ return slot.status === "active" ? slot : null;
5522
+ }
5418
5523
  try {
5419
5524
  otlpApp = await buildOtelReceiver({
5420
5525
  onSpan: async (span) => {
5421
- const liveEntries = await listProjects().catch(() => []);
5422
- const target = routeSpanToProject(span.service, liveEntries);
5423
- const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
5424
- if (!slot || slot.status !== "active") return;
5526
+ const slot = await resolveTargetSlot(span.service);
5527
+ if (!slot) return;
5425
5528
  await handleSpan(
5426
5529
  {
5427
5530
  graph: slot.graph,
@@ -5434,10 +5537,8 @@ async function startDaemon(opts = {}) {
5434
5537
  );
5435
5538
  },
5436
5539
  onErrorSpanSync: async (span) => {
5437
- const liveEntries = await listProjects().catch(() => []);
5438
- const target = routeSpanToProject(span.service, liveEntries);
5439
- const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
5440
- if (!slot || slot.status !== "active") return;
5540
+ const slot = await resolveTargetSlot(span.service);
5541
+ if (!slot) return;
5441
5542
  await makeErrorSpanWriter(slot.paths.errorsPath)(span);
5442
5543
  }
5443
5544
  });