@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/neatd.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startDaemon
4
- } from "./chunk-CB2UK4EH.js";
4
+ } from "./chunk-6GXBAR3M.js";
5
5
  import {
6
6
  listProjects,
7
7
  registryPath
8
- } from "./chunk-NTQHMXWE.js";
8
+ } from "./chunk-RBWL4HRB.js";
9
9
  import {
10
10
  BindAuthorityError,
11
11
  __require
12
- } from "./chunk-KYRIQIPG.js";
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-D5PIJFBE.js";
5
- import "./chunk-KYRIQIPG.js";
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-QTX2YQJZ.js.map
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 service = typeof resourceAttrs["service.name"] === "string" ? resourceAttrs["service.name"] : "unknown";
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 ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
449
- let body;
450
- let responseFlavor;
451
- if (ct === "application/x-protobuf") {
452
- responseFlavor = "protobuf";
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
- body = await decodeProtobufBody(req.body);
517
+ for (const span of spans) {
518
+ if (span.statusCode === 2) await opts.onErrorSpanSync(span);
519
+ }
455
520
  } catch (err) {
456
- return reply.code(400).send({
457
- error: `protobuf decode failed: ${err.message}`
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
- const spans = parseOtlpRequest(body);
467
- if (opts.onErrorSpanSync) {
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
- enqueue(spans);
479
- if (responseFlavor === "protobuf") {
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);