@atrim/instrument-node 0.7.1-dev.14fdea7.20260109021705 → 0.7.1-dev.764c183.20260110203528

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.
@@ -82,7 +82,7 @@ declare const createEffectTracingLayer: () => Layer.Layer<never>;
82
82
  declare const EffectTracingLive: Layer.Layer<never>;
83
83
  /**
84
84
  * Create a combined layer that provides both:
85
- * 1. Effect-native HTTP tracing (via NodeSdk.layer)
85
+ * 1. Effect-native HTTP tracing (via Effect's Tracer + global OTel provider)
86
86
  * 2. Fiber-level auto-tracing (via Supervisor)
87
87
  *
88
88
  * This gives you automatic spans for:
@@ -90,6 +90,14 @@ declare const EffectTracingLive: Layer.Layer<never>;
90
90
  * - Every forked fiber (from our Supervisor)
91
91
  *
92
92
  * No manual Effect.withSpan() calls needed.
93
+ *
94
+ * ARCHITECTURE NOTE:
95
+ * Unlike EffectTracingLive which uses NodeSdk.layer (scoped provider),
96
+ * CombinedTracingLive uses a single GLOBAL TracerProvider so that both
97
+ * Effect's Tracer and our Supervisor use the same provider and exporter.
98
+ *
99
+ * This solves the dual-provider problem where HTTP spans and fiber spans
100
+ * would otherwise go to different exporters.
93
101
  */
94
102
  declare const createCombinedTracingLayer: () => Layer.Layer<never>;
95
103
  /**
@@ -192,6 +200,10 @@ declare class AutoTracingSupervisor extends Supervisor.AbstractSupervisor<void>
192
200
  * Called when a fiber completes (success or failure)
193
201
  */
194
202
  onEnd<A, E>(exit: Exit.Exit<A, E>, fiber: Fiber.RuntimeFiber<A, E>): void;
203
+ /**
204
+ * Get attributes for span links from config
205
+ */
206
+ private getLinkAttributes;
195
207
  /**
196
208
  * Check if a span name should be traced based on filter patterns
197
209
  */
@@ -388,6 +400,13 @@ declare const AutoTracingConfig_base: Context.TagClass<AutoTracingConfig, "AutoT
388
400
  name: string;
389
401
  }[];
390
402
  };
403
+ span_relationships: {
404
+ type: "parent-child" | "span-links" | "both";
405
+ link_attributes?: {
406
+ "link.type": string;
407
+ custom?: Record<string, string> | undefined;
408
+ } | undefined;
409
+ };
391
410
  performance: {
392
411
  sampling_rate: number;
393
412
  min_duration: string;
@@ -82,7 +82,7 @@ declare const createEffectTracingLayer: () => Layer.Layer<never>;
82
82
  declare const EffectTracingLive: Layer.Layer<never>;
83
83
  /**
84
84
  * Create a combined layer that provides both:
85
- * 1. Effect-native HTTP tracing (via NodeSdk.layer)
85
+ * 1. Effect-native HTTP tracing (via Effect's Tracer + global OTel provider)
86
86
  * 2. Fiber-level auto-tracing (via Supervisor)
87
87
  *
88
88
  * This gives you automatic spans for:
@@ -90,6 +90,14 @@ declare const EffectTracingLive: Layer.Layer<never>;
90
90
  * - Every forked fiber (from our Supervisor)
91
91
  *
92
92
  * No manual Effect.withSpan() calls needed.
93
+ *
94
+ * ARCHITECTURE NOTE:
95
+ * Unlike EffectTracingLive which uses NodeSdk.layer (scoped provider),
96
+ * CombinedTracingLive uses a single GLOBAL TracerProvider so that both
97
+ * Effect's Tracer and our Supervisor use the same provider and exporter.
98
+ *
99
+ * This solves the dual-provider problem where HTTP spans and fiber spans
100
+ * would otherwise go to different exporters.
93
101
  */
94
102
  declare const createCombinedTracingLayer: () => Layer.Layer<never>;
95
103
  /**
@@ -192,6 +200,10 @@ declare class AutoTracingSupervisor extends Supervisor.AbstractSupervisor<void>
192
200
  * Called when a fiber completes (success or failure)
193
201
  */
194
202
  onEnd<A, E>(exit: Exit.Exit<A, E>, fiber: Fiber.RuntimeFiber<A, E>): void;
203
+ /**
204
+ * Get attributes for span links from config
205
+ */
206
+ private getLinkAttributes;
195
207
  /**
196
208
  * Check if a span name should be traced based on filter patterns
197
209
  */
@@ -388,6 +400,13 @@ declare const AutoTracingConfig_base: Context.TagClass<AutoTracingConfig, "AutoT
388
400
  name: string;
389
401
  }[];
390
402
  };
403
+ span_relationships: {
404
+ type: "parent-child" | "span-links" | "both";
405
+ link_attributes?: {
406
+ "link.type": string;
407
+ custom?: Record<string, string> | undefined;
408
+ } | undefined;
409
+ };
391
410
  performance: {
392
411
  sampling_rate: number;
393
412
  min_duration: string;
@@ -1,15 +1,15 @@
1
1
  import { Data, Context, Effect, Layer, FiberRef, Option, Supervisor, FiberRefs, Tracer, Exit } from 'effect';
2
- import { NodeSdk, Tracer as Tracer$1 } from '@effect/opentelemetry';
2
+ import { NodeSdk, Resource, Tracer as Tracer$1 } from '@effect/opentelemetry';
3
3
  import { BasicTracerProvider, SimpleSpanProcessor, ConsoleSpanExporter, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
4
4
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
5
+ import { resourceFromAttributes } from '@opentelemetry/resources';
6
+ import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
7
+ import * as OtelApi from '@opentelemetry/api';
5
8
  import { FileSystem } from '@effect/platform/FileSystem';
6
9
  import * as HttpClient from '@effect/platform/HttpClient';
7
10
  import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
8
11
  import { parse } from 'yaml';
9
12
  import { z } from 'zod';
10
- import * as OtelApi from '@opentelemetry/api';
11
- import { resourceFromAttributes } from '@opentelemetry/resources';
12
- import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
13
13
  import * as path from 'path';
14
14
 
15
15
  var __defProp = Object.defineProperty;
@@ -82,6 +82,25 @@ var AutoInstrumentationConfigSchema = z.object({
82
82
  // Naming rules (first match wins)
83
83
  rules: z.array(SpanNamingRuleSchema).default([])
84
84
  }).default({}),
85
+ // Span relationship configuration for forked fibers
86
+ // Controls how child fiber spans relate to their parent/forking context
87
+ span_relationships: z.object({
88
+ // Relationship type between forked fiber spans and their parent context
89
+ // - 'parent-child': Use parent-child relationship (default, traditional tracing)
90
+ // Parent span shows child as nested. Works well with most observability tools.
91
+ // - 'span-links': Use span links (semantically correct for async forks per OTel spec)
92
+ // Fibers get independent traces linked to parent. Better for long-running fibers.
93
+ // - 'both': Create parent-child AND add span links
94
+ // Maximum visibility but may create redundant data.
95
+ type: z.enum(["parent-child", "span-links", "both"]).default("parent-child"),
96
+ // Custom attributes to add to span links (only used when type includes links)
97
+ link_attributes: z.object({
98
+ // Link type identifier
99
+ "link.type": z.string().default("fork"),
100
+ // Custom attributes (key-value pairs)
101
+ custom: z.record(z.string()).optional()
102
+ }).optional()
103
+ }).default({}),
85
104
  // Pattern-based filtering
86
105
  filter: z.object({
87
106
  // Only trace spans matching these patterns (empty = trace all)
@@ -525,6 +544,9 @@ var defaultAutoTracingConfig = {
525
544
  infer_from_source: true,
526
545
  rules: []
527
546
  },
547
+ span_relationships: {
548
+ type: "parent-child"
549
+ },
528
550
  filter: {
529
551
  include: [],
530
552
  exclude: []
@@ -792,42 +814,85 @@ var AutoTracingSupervisor = class extends Supervisor.AbstractSupervisor {
792
814
  if (!this.shouldTrace(spanName)) {
793
815
  return;
794
816
  }
817
+ const relationshipType = this.config.span_relationships?.type ?? "parent-child";
818
+ const useParentChild = relationshipType === "parent-child" || relationshipType === "both";
819
+ const useSpanLinks = relationshipType === "span-links" || relationshipType === "both";
795
820
  let parentContext = OtelApi.ROOT_CONTEXT;
796
821
  let parentFiberId;
822
+ let spanLinks = [];
797
823
  const maybeEffectParentSpan = Context.getOption(_context, Tracer.ParentSpan);
798
824
  if (Option.isSome(maybeEffectParentSpan)) {
799
825
  const effectSpan = maybeEffectParentSpan.value;
800
- logger.log(`@atrim/auto-trace: Found ParentSpan - traceId=${effectSpan.traceId.slice(0, 8)}..., spanId=${effectSpan.spanId.slice(0, 8)}...`);
826
+ logger.log(
827
+ `@atrim/auto-trace: Found ParentSpan - traceId=${effectSpan.traceId.slice(0, 8)}..., spanId=${effectSpan.spanId.slice(0, 8)}...`
828
+ );
801
829
  const otelSpanContext = {
802
830
  traceId: effectSpan.traceId,
803
831
  spanId: effectSpan.spanId,
804
832
  traceFlags: effectSpan.sampled ? OtelApi.TraceFlags.SAMPLED : OtelApi.TraceFlags.NONE,
805
833
  isRemote: false
806
834
  };
807
- const wrappedSpan = OtelApi.trace.wrapSpanContext(otelSpanContext);
808
- parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, wrappedSpan);
835
+ if (useParentChild) {
836
+ const wrappedSpan = OtelApi.trace.wrapSpanContext(otelSpanContext);
837
+ parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, wrappedSpan);
838
+ }
839
+ if (useSpanLinks) {
840
+ const linkAttributes = this.getLinkAttributes();
841
+ spanLinks.push({
842
+ context: otelSpanContext,
843
+ attributes: linkAttributes
844
+ });
845
+ logger.log(`@atrim/auto-trace: Added span link to parent (${relationshipType})`);
846
+ }
809
847
  } else if (Option.isSome(parent)) {
810
848
  parentFiberId = parent.value.id().id;
811
849
  const parentSpan = this.fiberSpans.get(parent.value);
812
850
  if (parentSpan) {
813
- parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, parentSpan);
851
+ if (useParentChild) {
852
+ parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, parentSpan);
853
+ }
854
+ if (useSpanLinks) {
855
+ const linkAttributes = this.getLinkAttributes();
856
+ spanLinks.push({
857
+ context: parentSpan.spanContext(),
858
+ attributes: linkAttributes
859
+ });
860
+ }
814
861
  } else if (this._rootSpan) {
815
- parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, this._rootSpan);
862
+ if (useParentChild) {
863
+ parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, this._rootSpan);
864
+ }
865
+ if (useSpanLinks) {
866
+ const linkAttributes = this.getLinkAttributes();
867
+ spanLinks.push({
868
+ context: this._rootSpan.spanContext(),
869
+ attributes: linkAttributes
870
+ });
871
+ }
816
872
  }
817
873
  } else if (this._rootSpan) {
818
- parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, this._rootSpan);
874
+ if (useParentChild) {
875
+ parentContext = OtelApi.trace.setSpan(OtelApi.ROOT_CONTEXT, this._rootSpan);
876
+ }
877
+ if (useSpanLinks) {
878
+ const linkAttributes = this.getLinkAttributes();
879
+ spanLinks.push({
880
+ context: this._rootSpan.spanContext(),
881
+ attributes: linkAttributes
882
+ });
883
+ }
819
884
  }
820
885
  if (Option.isSome(parent)) {
821
886
  parentFiberId = parent.value.id().id;
822
887
  }
823
- const span = this.tracer.startSpan(
824
- spanName,
825
- {
826
- kind: OtelApi.SpanKind.INTERNAL,
827
- attributes: this.getInitialAttributes(fiber, sourceInfo, parentFiberId)
828
- },
829
- parentContext
830
- );
888
+ const spanOptions = {
889
+ kind: OtelApi.SpanKind.INTERNAL,
890
+ attributes: this.getInitialAttributes(fiber, sourceInfo, parentFiberId)
891
+ };
892
+ if (spanLinks.length > 0) {
893
+ spanOptions.links = spanLinks;
894
+ }
895
+ const span = this.tracer.startSpan(spanName, spanOptions, parentContext);
831
896
  logger.log(`@atrim/auto-trace: Created span "${spanName}" for fiber ${fiber.id().id}`);
832
897
  this.fiberSpans.set(fiber, span);
833
898
  this.fiberStartTimes.set(fiber, process.hrtime.bigint());
@@ -837,8 +902,12 @@ var AutoTracingSupervisor = class extends Supervisor.AbstractSupervisor {
837
902
  * Called when a fiber completes (success or failure)
838
903
  */
839
904
  onEnd(exit, fiber) {
905
+ logger.log(`@atrim/auto-trace: onEnd called for fiber ${fiber.id().id}`);
840
906
  const span = this.fiberSpans.get(fiber);
841
907
  if (!span) {
908
+ logger.log(
909
+ `@atrim/auto-trace: No span found for fiber ${fiber.id().id} (skipped or filtered)`
910
+ );
842
911
  return;
843
912
  }
844
913
  const startTime = this.fiberStartTimes.get(fiber);
@@ -862,10 +931,26 @@ var AutoTracingSupervisor = class extends Supervisor.AbstractSupervisor {
862
931
  span.setAttribute("effect.fiber.failed", true);
863
932
  }
864
933
  span.end();
934
+ logger.log(`@atrim/auto-trace: Ended span for fiber ${fiber.id().id}`);
865
935
  this.fiberSpans.delete(fiber);
866
936
  this.fiberStartTimes.delete(fiber);
867
937
  this.activeFiberCount--;
868
938
  }
939
+ /**
940
+ * Get attributes for span links from config
941
+ */
942
+ getLinkAttributes() {
943
+ const linkConfig = this.config.span_relationships?.link_attributes;
944
+ const attrs = {
945
+ "link.type": linkConfig?.["link.type"] ?? "fork"
946
+ };
947
+ if (linkConfig?.custom) {
948
+ for (const [key, value] of Object.entries(linkConfig.custom)) {
949
+ attrs[key] = value;
950
+ }
951
+ }
952
+ return attrs;
953
+ }
869
954
  /**
870
955
  * Check if a span name should be traced based on filter patterns
871
956
  */
@@ -1037,7 +1122,9 @@ var createExporterLayer = (exporterConfig, serviceName, serviceVersion) => {
1037
1122
  };
1038
1123
  if (config.headers) {
1039
1124
  exporterConfig2.headers = config.headers;
1040
- logger.log(`@atrim/auto-trace: Using custom headers: ${Object.keys(config.headers).join(", ")}`);
1125
+ logger.log(
1126
+ `@atrim/auto-trace: Using custom headers: ${Object.keys(config.headers).join(", ")}`
1127
+ );
1041
1128
  }
1042
1129
  return new OTLPTraceExporter(exporterConfig2);
1043
1130
  };
@@ -1130,7 +1217,9 @@ var createEffectTracingLayer = () => {
1130
1217
  };
1131
1218
  if (exporterConfig.headers) {
1132
1219
  otlpConfig.headers = exporterConfig.headers;
1133
- logger.log(`@atrim/auto-trace: Using custom headers: ${Object.keys(exporterConfig.headers).join(", ")}`);
1220
+ logger.log(
1221
+ `@atrim/auto-trace: Using custom headers: ${Object.keys(exporterConfig.headers).join(", ")}`
1222
+ );
1134
1223
  }
1135
1224
  const exporter = new OTLPTraceExporter(otlpConfig);
1136
1225
  if (exporterConfig.processor === "simple") {
@@ -1178,6 +1267,9 @@ var createCombinedTracingLayer = () => {
1178
1267
  infer_from_source: true,
1179
1268
  rules: []
1180
1269
  },
1270
+ span_relationships: {
1271
+ type: "parent-child"
1272
+ },
1181
1273
  filter: { include: [], exclude: [] },
1182
1274
  performance: { sampling_rate: 1, min_duration: "0ms", max_concurrent: 0 },
1183
1275
  metadata: { fiber_info: true, source_location: true, parent_fiber: true }
@@ -1189,10 +1281,10 @@ var createCombinedTracingLayer = () => {
1189
1281
  logger.log('@atrim/auto-trace: Exporter type is "none", using empty layer');
1190
1282
  return Layer.empty;
1191
1283
  }
1192
- const createSpanProcessor = () => {
1284
+ const createSpanExporter = () => {
1193
1285
  if (exporterConfig.type === "console") {
1194
- logger.log("@atrim/auto-trace: Using ConsoleSpanExporter with SimpleSpanProcessor");
1195
- return new SimpleSpanProcessor(new ConsoleSpanExporter());
1286
+ logger.log("@atrim/auto-trace: Using ConsoleSpanExporter");
1287
+ return new ConsoleSpanExporter();
1196
1288
  }
1197
1289
  const endpoint = exporterConfig.endpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
1198
1290
  logger.log(`@atrim/auto-trace: Using OTLPTraceExporter (${endpoint})`);
@@ -1201,10 +1293,15 @@ var createCombinedTracingLayer = () => {
1201
1293
  };
1202
1294
  if (exporterConfig.headers) {
1203
1295
  otlpConfig.headers = exporterConfig.headers;
1204
- logger.log(`@atrim/auto-trace: Using custom headers: ${Object.keys(exporterConfig.headers).join(", ")}`);
1296
+ logger.log(
1297
+ `@atrim/auto-trace: Using custom headers: ${Object.keys(exporterConfig.headers).join(", ")}`
1298
+ );
1205
1299
  }
1206
- const exporter = new OTLPTraceExporter(otlpConfig);
1207
- if (exporterConfig.processor === "simple") {
1300
+ return new OTLPTraceExporter(otlpConfig);
1301
+ };
1302
+ const createSpanProcessor = () => {
1303
+ const exporter = createSpanExporter();
1304
+ if (exporterConfig.processor === "simple" || exporterConfig.type === "console") {
1208
1305
  logger.log("@atrim/auto-trace: Using SimpleSpanProcessor");
1209
1306
  return new SimpleSpanProcessor(exporter);
1210
1307
  }
@@ -1218,34 +1315,31 @@ var createCombinedTracingLayer = () => {
1218
1315
  maxExportBatchSize: batchConfig.max_export_batch_size
1219
1316
  });
1220
1317
  };
1221
- const sdkLayer = NodeSdk.layer(() => ({
1222
- resource: {
1223
- serviceName,
1224
- serviceVersion
1225
- },
1226
- spanProcessor: createSpanProcessor()
1227
- }));
1228
- const supervisorLayer = Layer.unwrapScoped(
1229
- Effect.gen(function* () {
1230
- const tracerProvider = yield* Effect.serviceOption(Tracer$1.OtelTracerProvider);
1231
- if (Option.isSome(tracerProvider)) {
1232
- logger.log("@atrim/auto-trace: Got TracerProvider from NodeSdk layer");
1233
- const supervisor = createAutoTracingSupervisor(autoConfig, tracerProvider.value);
1234
- logger.log("@atrim/auto-trace: Combined layer created");
1235
- logger.log(" - HTTP requests: auto-traced via Effect platform");
1236
- logger.log(" - Forked fibers: auto-traced via Supervisor");
1237
- return Supervisor.addSupervisor(supervisor);
1238
- } else {
1239
- logger.log("@atrim/auto-trace: WARNING: No TracerProvider found, creating supervisor without it");
1240
- const supervisor = createAutoTracingSupervisor(autoConfig);
1241
- logger.log("@atrim/auto-trace: Combined layer created (fallback)");
1242
- logger.log(" - HTTP requests: auto-traced via Effect platform");
1243
- logger.log(" - Forked fibers: auto-traced via Supervisor");
1244
- return Supervisor.addSupervisor(supervisor);
1245
- }
1318
+ const globalProviderLayer = Layer.effectDiscard(
1319
+ Effect.sync(() => {
1320
+ const provider = new BasicTracerProvider({
1321
+ resource: resourceFromAttributes({
1322
+ [ATTR_SERVICE_NAME]: serviceName,
1323
+ [ATTR_SERVICE_VERSION]: serviceVersion
1324
+ }),
1325
+ spanProcessors: [createSpanProcessor()]
1326
+ });
1327
+ OtelApi.trace.setGlobalTracerProvider(provider);
1328
+ logger.log("@atrim/auto-trace: Global TracerProvider registered");
1246
1329
  })
1247
1330
  );
1248
- return supervisorLayer.pipe(Layer.provide(Layer.discard(sdkLayer)));
1331
+ const resourceLayer = Resource.layer({
1332
+ serviceName,
1333
+ serviceVersion
1334
+ });
1335
+ const effectTracerLayer = Tracer$1.layerGlobal;
1336
+ const supervisor = createAutoTracingSupervisor(autoConfig);
1337
+ const supervisorLayer = Supervisor.addSupervisor(supervisor);
1338
+ logger.log("@atrim/auto-trace: Combined layer created");
1339
+ logger.log(" - HTTP requests: auto-traced via Effect platform (global provider)");
1340
+ logger.log(" - Forked fibers: auto-traced via Supervisor (global provider)");
1341
+ const tracerWithResource = effectTracerLayer.pipe(Layer.provide(resourceLayer));
1342
+ return Layer.mergeAll(globalProviderLayer, Layer.discard(tracerWithResource), supervisorLayer);
1249
1343
  })
1250
1344
  );
1251
1345
  };