@m1212e/rumble 0.23.17 → 0.23.19

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/out/index.mjs CHANGED
@@ -3,15 +3,16 @@ import { t as generateFromSchema } from "./generate-0QFVc3Sg.mjs";
3
3
  import { GraphQLError, GraphQLSchema, execute as defaultExecute, getOperationAST, print, subscribe as defaultSubscribe } from "graphql";
4
4
  import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
5
5
  import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection";
6
- import { SpanStatusCode, trace } from "@opentelemetry/api";
6
+ import { SpanStatusCode, isSpanContextValid, trace } from "@opentelemetry/api";
7
7
  import { capitalize, cloneDeep, debounce, merge, toMerged } from "es-toolkit";
8
8
  import { createPubSub, createYoga } from "graphql-yoga";
9
9
  import { One, count, getTableColumns, getTableName, is, isTable, relationsFilterToSQL, sql } from "drizzle-orm";
10
+ import { ATTR_GRAPHQL_DOCUMENT, ATTR_GRAPHQL_OPERATION_NAME, ATTR_GRAPHQL_OPERATION_TYPE } from "@opentelemetry/semantic-conventions/incubating";
11
+ import { AttributeNames, SpanNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
10
12
  import { toCamelCase } from "drizzle-orm/casing";
11
13
  import { PgColumn, PgEnumColumn, PgEnumObjectColumn, PgTable, isPgEnum } from "drizzle-orm/pg-core";
12
14
  import { MySqlEnumColumn, MySqlEnumObjectColumn, MySqlTable } from "drizzle-orm/mysql-core";
13
15
  import pluralize from "pluralize";
14
- import { AttributeNames, SpanNames, createOpenTelemetryWrapper } from "@pothos/tracing-opentelemetry";
15
16
  import { SQLiteTable } from "drizzle-orm/sqlite-core";
16
17
  import SchemaBuilder, { BasePlugin } from "@pothos/core";
17
18
  import DrizzlePlugin from "@pothos/plugin-drizzle";
@@ -181,7 +182,39 @@ function mapNullFieldsToUndefined(obj) {
181
182
  }
182
183
  //#endregion
183
184
  //#region package.json
184
- var version = "0.23.17";
185
+ var version = "0.23.19";
186
+ //#endregion
187
+ //#region lib/helpers/errorLogging.ts
188
+ function hasMessage(error) {
189
+ return typeof error === "object" && error !== null && "message" in error;
190
+ }
191
+ function toLoggedException(error) {
192
+ if (hasMessage(error)) {
193
+ const gqlError = error;
194
+ return {
195
+ "exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
196
+ "exception.message": String(gqlError.message),
197
+ ...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
198
+ ...gqlError.path ? { path: gqlError.path } : {},
199
+ ...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
200
+ };
201
+ }
202
+ return {
203
+ "exception.type": "UnknownError",
204
+ "exception.message": String(error)
205
+ };
206
+ }
207
+ /**
208
+ * Every erroneous rumble log carries the same `errors` array shape,
209
+ * whether it originates from a thrown exception (single-element array)
210
+ * or a GraphQL `ExecutionResult.errors` list (multi-element array).
211
+ */
212
+ function errorsLogField(errors) {
213
+ return { errors: errors.map(toLoggedException) };
214
+ }
215
+ function errorLogField(error) {
216
+ return errorsLogField([error]);
217
+ }
185
218
  //#endregion
186
219
  //#region lib/helpers/mergeFilters.ts
187
220
  const EmptyFilter$1 = Symbol.for("drizzle:EmptyFilter");
@@ -441,6 +474,182 @@ function tableHelper({ db, table }) {
441
474
  };
442
475
  }
443
476
  //#endregion
477
+ //#region lib/helpers/telemetry.ts
478
+ const SPAN_EXECUTE = SpanNames.EXECUTE;
479
+ /** @pothos/tracing-opentelemetry's SpanNames enum has no subscribe member. */
480
+ const SPAN_SUBSCRIBE = "graphql.subscribe";
481
+ const SPAN_SUBSCRIBE_EVENT = "graphql.subscribe.event";
482
+ const SPAN_ABILITIES_PREPARE = "rumble.abilities.prepare";
483
+ const SPAN_FILTERS_APPLY = "rumble.filters.apply";
484
+ const SPAN_FILTERS_RESOLVE = "rumble.filters.resolve";
485
+ const ATTR_OPERATION_NAME = ATTR_GRAPHQL_OPERATION_NAME;
486
+ const ATTR_OPERATION_TYPE = ATTR_GRAPHQL_OPERATION_TYPE;
487
+ const ATTR_DOCUMENT = ATTR_GRAPHQL_DOCUMENT;
488
+ const ATTR_FIELD_NAME = AttributeNames.FIELD_NAME;
489
+ /** Prefix for per-variable span attributes, e.g. `graphql.variables.userId`. */
490
+ const ATTR_VARIABLES_PREFIX = AttributeNames.VARIABLES;
491
+ /** Log field holding all variables as one object (logs keep nesting, spans flatten). */
492
+ const FIELD_VARIABLES = "graphql.variables";
493
+ const ATTR_PARENT_TYPE = "graphql.parent.type";
494
+ const ATTR_TRANSPORT = "rumble.transport";
495
+ const ATTR_TABLE = "rumble.table";
496
+ const ATTR_ACTION = "rumble.action";
497
+ const ATTR_ABILITIES_STATUS = "rumble.abilities.status";
498
+ const ATTR_ABILITIES_STATIC = "rumble.abilities.static";
499
+ const ATTR_ABILITIES_DYNAMIC = "rumble.abilities.dynamic";
500
+ const ATTR_ABILITIES_TOTAL = "rumble.abilities.total";
501
+ const ATTR_FILTERS_TOTAL = "rumble.filters.total";
502
+ const ATTR_FILTERS_ALLOWED = "rumble.filters.allowed";
503
+ const ATTR_SUBSCRIPTION_EVENT_INDEX = "rumble.subscription.event_index";
504
+ const FIELD_DURATION_MS = "duration_ms";
505
+ const FIELD_EVENT_COUNT = "event_count";
506
+ function telemetryLogger(config) {
507
+ return config.logger?.enabled ? config.logger.logger : void 0;
508
+ }
509
+ function telemetryTracer(config) {
510
+ return config.otel?.enabled ? config.otel.tracer : void 0;
511
+ }
512
+ /**
513
+ * Whether anything at all should be instrumented. When this is false, rumble
514
+ * hands the untouched execute/subscribe functions back to the transports so a
515
+ * user who wants neither traces nor logs pays no overhead.
516
+ */
517
+ function telemetryEnabled(config) {
518
+ return Boolean(config.otel?.enabled || config.logger?.enabled);
519
+ }
520
+ /**
521
+ * `trace_id`/`span_id`/`trace_flags` for the given span, or for the currently
522
+ * active one when no span is passed. Field names and the two-hex-digit flags
523
+ * format follow the OpenTelemetry log data model, which is what log backends
524
+ * (Loki, Tempo, Elastic) expect when linking a log line to its trace.
525
+ *
526
+ * Reading the *active* span means every rumble log site gets correlation for
527
+ * free, without threading a child logger through the whole library.
528
+ */
529
+ function traceCorrelationFields(config, span) {
530
+ if (!config.otel?.enabled || config.logger?.injectTraceId === false) return {};
531
+ const target = span ?? trace.getActiveSpan();
532
+ if (!target) return {};
533
+ const spanContext = target.spanContext();
534
+ if (!isSpanContextValid(spanContext)) return {};
535
+ return {
536
+ trace_id: spanContext.traceId,
537
+ span_id: spanContext.spanId,
538
+ trace_flags: spanContext.traceFlags.toString(16).padStart(2, "0")
539
+ };
540
+ }
541
+ function toException(error) {
542
+ if (error instanceof Error) return error;
543
+ if (typeof error === "object" && error !== null && "message" in error) return error;
544
+ return String(error);
545
+ }
546
+ function exceptionMessage(error) {
547
+ if (typeof error === "object" && error !== null && "message" in error) return String(error.message);
548
+ return error === void 0 ? void 0 : String(error);
549
+ }
550
+ /**
551
+ * Records every error as an exception event and flips the span to ERROR, per
552
+ * OpenTelemetry's exception conventions. Used for rumble's own spans as well as
553
+ * the GraphQL operation spans so a failure looks identical wherever it happens.
554
+ */
555
+ function recordSpanErrors(span, errors) {
556
+ if (!span || errors.length === 0) return;
557
+ for (const error of errors) span.recordException(toException(error));
558
+ span.setStatus({
559
+ code: SpanStatusCode.ERROR,
560
+ message: exceptionMessage(errors[0])
561
+ });
562
+ }
563
+ function recordSpanError(span, error) {
564
+ recordSpanErrors(span, [error]);
565
+ }
566
+ /**
567
+ * Errors the operation wrapper has already reported.
568
+ *
569
+ * A transport can see the same failure twice: sofa, for instance, hands
570
+ * `result.errors` to its error handler *after* execute has returned, so without
571
+ * this the very same error would produce two log lines and two exception events,
572
+ * which quietly doubles error counts in dashboards. A WeakSet keeps this free of
573
+ * bookkeeping — entries disappear with the errors themselves.
574
+ */
575
+ const reportedErrors = /* @__PURE__ */ new WeakSet();
576
+ function markErrorsReported(errors) {
577
+ for (const error of errors) if (typeof error === "object" && error !== null) reportedErrors.add(error);
578
+ }
579
+ /** True when every error was already reported by the operation wrapper. */
580
+ function errorsAlreadyReported(errors) {
581
+ if (errors.length === 0) return false;
582
+ return errors.every((error) => typeof error === "object" && error !== null && reportedErrors.has(error));
583
+ }
584
+ function safeJsonStringify(value) {
585
+ try {
586
+ return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner) ?? "null";
587
+ } catch {
588
+ return String(value);
589
+ }
590
+ }
591
+ function isHomogeneousPrimitiveArray(value) {
592
+ let seen;
593
+ for (const entry of value) {
594
+ if (entry === null || entry === void 0) continue;
595
+ const type = typeof entry;
596
+ if (type !== "string" && type !== "number" && type !== "boolean") return false;
597
+ if (seen && seen !== type) return false;
598
+ seen = type;
599
+ }
600
+ return true;
601
+ }
602
+ /**
603
+ * Coerces an arbitrary value into something the OpenTelemetry attribute type
604
+ * allows: primitives pass through, homogeneous primitive arrays stay arrays
605
+ * (mixed ones are invalid per spec) and everything else becomes JSON.
606
+ *
607
+ * Values are intentionally not truncated here — length limits belong to the SDK
608
+ * (`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`) so they stay configurable per deployment.
609
+ */
610
+ function attributeValue(value) {
611
+ switch (typeof value) {
612
+ case "string": return value;
613
+ case "number": return Number.isFinite(value) ? value : String(value);
614
+ case "boolean": return value;
615
+ case "bigint": return value.toString();
616
+ case "undefined": return "null";
617
+ default: break;
618
+ }
619
+ if (value === null) return "null";
620
+ if (Array.isArray(value) && isHomogeneousPrimitiveArray(value)) return value;
621
+ return safeJsonStringify(value);
622
+ }
623
+ function resolveVariables(variables, include) {
624
+ if (include === false || !variables) return void 0;
625
+ const resolved = typeof include === "function" ? include({ ...variables }) : variables;
626
+ if (!resolved || Object.keys(resolved).length === 0) return void 0;
627
+ return resolved;
628
+ }
629
+ /** Variables flattened into `graphql.variables.<name>` span attributes. */
630
+ function variableAttributes(variables, include) {
631
+ const resolved = resolveVariables(variables, include);
632
+ if (!resolved) return {};
633
+ const attributes = {};
634
+ for (const [key, value] of Object.entries(resolved)) attributes[`${ATTR_VARIABLES_PREFIX}${key}`] = attributeValue(value);
635
+ return attributes;
636
+ }
637
+ /** Variables as a single nested `graphql.variables` log field. */
638
+ function variableLogField(variables, include) {
639
+ const resolved = resolveVariables(variables, include);
640
+ return resolved ? { [FIELD_VARIABLES]: resolved } : {};
641
+ }
642
+ /**
643
+ * Monotonic clock reading. `Date.now()` can jump backwards when the wall clock is
644
+ * adjusted (NTP), which would produce negative or inflated durations.
645
+ */
646
+ function startTimer() {
647
+ return performance.now();
648
+ }
649
+ function durationMs(start) {
650
+ return Math.round((performance.now() - start) * 100) / 100;
651
+ }
652
+ //#endregion
444
653
  //#region lib/abilityBuilder.ts
445
654
  function isDynamicQueryFilter(filter) {
446
655
  return typeof filter === "function" && filter.constructor.name !== "AsyncFunction";
@@ -448,17 +657,22 @@ function isDynamicQueryFilter(filter) {
448
657
  function isStaticQueryFilter(filter) {
449
658
  return typeof filter !== "function";
450
659
  }
451
- const makeNothingRegisteredWarner = (logger) => debounce((model, action) => {
660
+ const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
452
661
  const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
453
662
  if (logger) logger.warn({
454
- "rumble.table": model,
455
- "rumble.action": action
663
+ [ATTR_TABLE]: model,
664
+ [ATTR_ACTION]: action,
665
+ ...traceCorrelationFields(config)
456
666
  }, msg);
457
667
  else console.warn(msg);
458
668
  }, 1e3);
459
669
  const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
460
670
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
461
- const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(log);
671
+ const telemetryConfig = {
672
+ otel,
673
+ logger: loggerConfig
674
+ };
675
+ const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
462
676
  const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
463
677
  db,
464
678
  table: name
@@ -718,14 +932,14 @@ by: (explicitFilter) => {
718
932
  }
719
933
  return { withContext: (userContext) => {
720
934
  return { filter: (action) => {
721
- const assembleAbilities = (span) => {
935
+ const assembleAbilities = (attributes) => {
722
936
  const filters = queryFilters.get(action);
723
937
  if (filters === "unrestricted") {
724
- span?.setAttribute("abilities.status", "unrestricted");
938
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
725
939
  return transformToResponse();
726
940
  }
727
941
  if (!filters) {
728
- span?.setAttribute("abilities.status", "blocked_everything");
942
+ attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
729
943
  nothingRegisteredWarningLogger(String(tableName), action);
730
944
  return transformToResponse(blockEverythingFilter);
731
945
  }
@@ -734,58 +948,60 @@ by: (explicitFilter) => {
734
948
  for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
735
949
  const func = dynamicQueryFilters[action][i];
736
950
  const result = func(userContext);
737
- if (result === "allow") return transformToResponse();
951
+ if (result === "allow") {
952
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
953
+ return transformToResponse();
954
+ }
738
955
  if (result === void 0) continue;
739
956
  dynamicResults[filtersReturned++] = result;
740
957
  }
741
958
  dynamicResults.length = filtersReturned;
742
- span?.setAttribute("abilities.dynamic", dynamicResults.length);
743
- span?.setAttribute("abilities.static", simpleQueryFilters[action].length);
959
+ attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
960
+ attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
744
961
  const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
745
- span?.setAttribute("abilities.total", allQueryFilters.length);
962
+ attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
746
963
  if (allQueryFilters.length === 0) {
747
- span?.setAttribute("abilities.status", "blocked_everything");
964
+ attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
748
965
  return transformToResponse(blockEverythingFilter);
749
966
  }
750
967
  const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
751
968
  return mergeFilters(a, b, "OR");
752
969
  });
753
- span?.setAttribute("abilities.status", "applied");
970
+ attributes[ATTR_ABILITIES_STATUS] = "applied";
754
971
  return transformToResponse(mergedFilters);
755
972
  };
756
- if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`rumble.abilities.prepare`, (span) => {
757
- span.setAttribute("rumble.action", action);
758
- span.setAttribute("rumble.table", String(tableName));
973
+ const run = (span) => {
974
+ const attributes = {
975
+ [ATTR_TABLE]: String(tableName),
976
+ [ATTR_ACTION]: action
977
+ };
759
978
  try {
760
- const attrs = {
761
- "rumble.table": String(tableName),
762
- "rumble.action": action
763
- };
764
- const proxy = new Proxy(span, { get(target, prop) {
765
- if (prop === "setAttribute") return (k, v) => {
766
- attrs[k] = v;
767
- return target.setAttribute(k, v);
768
- };
769
- return target[prop];
770
- } });
771
- const result = assembleAbilities(proxy);
772
- log?.debug(attrs, "abilities prepared");
979
+ const result = assembleAbilities(attributes);
980
+ log?.debug({
981
+ ...attributes,
982
+ ...traceCorrelationFields(telemetryConfig, span)
983
+ }, "abilities prepared");
773
984
  return result;
985
+ } catch (error) {
986
+ recordSpanError(span, error);
987
+ log?.error({
988
+ ...attributes,
989
+ ...traceCorrelationFields(telemetryConfig, span),
990
+ ...errorLogField(error)
991
+ }, "abilities failed");
992
+ throw error;
993
+ } finally {
994
+ span?.setAttributes(attributes);
995
+ }
996
+ };
997
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, (span) => {
998
+ try {
999
+ return run(span);
774
1000
  } finally {
775
1001
  span.end();
776
1002
  }
777
1003
  });
778
- else {
779
- const attrs = {
780
- "rumble.table": String(tableName),
781
- "rumble.action": action
782
- };
783
- const result = assembleAbilities(log ? { setAttribute: (k, v) => {
784
- attrs[k] = v;
785
- } } : void 0);
786
- log?.debug(attrs, "abilities prepared");
787
- return result;
788
- }
1004
+ return run();
789
1005
  } };
790
1006
  } };
791
1007
  };
@@ -1312,207 +1528,192 @@ const sofaOpenAPIWebhookDocs = {
1312
1528
  } }
1313
1529
  };
1314
1530
  //#endregion
1315
- //#region lib/helpers/errorLogging.ts
1316
- function hasMessage(error) {
1317
- return typeof error === "object" && error !== null && "message" in error;
1531
+ //#region lib/helpers/tracing.ts
1532
+ function isAsyncIterable(value) {
1533
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
1318
1534
  }
1319
- function toLoggedException(error) {
1320
- if (hasMessage(error)) {
1321
- const gqlError = error;
1322
- return {
1323
- "exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
1324
- "exception.message": String(gqlError.message),
1325
- ...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
1326
- ...gqlError.path ? { path: gqlError.path } : {},
1327
- ...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
1328
- };
1329
- }
1535
+ function buildOperationTelemetry(config, transport, options) {
1536
+ const operationAst = getOperationAST(options.document, options.operationName);
1537
+ const operationName = options.operationName ?? operationAst?.name?.value ?? "anonymous";
1538
+ const operationType = operationAst?.operation;
1539
+ const logFields = {
1540
+ [ATTR_OPERATION_NAME]: operationName,
1541
+ ...operationType ? { [ATTR_OPERATION_TYPE]: operationType } : {},
1542
+ [ATTR_TRANSPORT]: transport
1543
+ };
1330
1544
  return {
1331
- "exception.type": "UnknownError",
1332
- "exception.message": String(error)
1545
+ operationName,
1546
+ operationType,
1547
+ logFields,
1548
+ variableLogFields: variableLogField(options.variableValues, config.logger?.includeVariables),
1549
+ attributes: () => ({
1550
+ ...logFields,
1551
+ ...config.otel?.includeDocument === false ? {} : { [ATTR_DOCUMENT]: print(options.document) },
1552
+ ...variableAttributes(options.variableValues, config.otel?.includeVariables)
1553
+ })
1333
1554
  };
1334
1555
  }
1335
1556
  /**
1336
- * Every erroneous rumble log carries the same `errors` array shape,
1337
- * whether it originates from a thrown exception (single-element array)
1338
- * or a GraphQL `ExecutionResult.errors` list (multi-element array).
1557
+ * Wraps a subscription's event stream so delivered events are observable no
1558
+ * matter which transport consumes them.
1339
1559
  */
1340
- function errorsLogField(errors) {
1341
- return { errors: errors.map(toLoggedException) };
1342
- }
1343
- function errorLogField(error) {
1344
- return errorsLogField([error]);
1345
- }
1346
- //#endregion
1347
- //#region lib/helpers/tracing.ts
1348
- const SUBSCRIBE_SPAN_NAME = "graphql.subscribe";
1349
- function getOperationType(options) {
1350
- return getOperationAST(options.document, options.operationName)?.operation;
1351
- }
1352
- function operationLogFields(operationName, operationType) {
1353
- return {
1354
- "graphql.operation.name": operationName,
1355
- ...operationType ? { "graphql.operation.type": operationType } : {}
1356
- };
1357
- }
1358
- function traceIdLogFields(span) {
1359
- const { traceId, spanId, traceFlags } = span.spanContext();
1360
- return {
1361
- trace_id: traceId,
1362
- span_id: spanId,
1363
- trace_flags: traceFlags.toString(16).padStart(2, "0")
1560
+ async function* wrapSubscriptionIterator(iterator, telemetry) {
1561
+ const { log, tracer, setupSpanContext, logFields, attributes } = telemetry;
1562
+ const recordEventErrors = (eventCount, errors) => {
1563
+ if (!tracer) return;
1564
+ const span = tracer.startSpan(SPAN_SUBSCRIBE_EVENT, {
1565
+ root: true,
1566
+ links: setupSpanContext ? [{ context: setupSpanContext }] : void 0,
1567
+ attributes: {
1568
+ ...attributes,
1569
+ [ATTR_SUBSCRIPTION_EVENT_INDEX]: eventCount
1570
+ }
1571
+ });
1572
+ recordSpanErrors(span, errors);
1573
+ span.end();
1364
1574
  };
1365
- }
1366
- function isAsyncIterable(value) {
1367
- return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
1368
- }
1369
- async function* wrapSubscriptionIterator(iterator, log, operationName, operationType) {
1370
1575
  let eventCount = 0;
1371
1576
  try {
1372
1577
  for await (const event of iterator) {
1373
1578
  eventCount++;
1374
- if (event.errors?.length) log.error({
1375
- ...operationLogFields(operationName, operationType),
1376
- event_count: eventCount,
1377
- ...errorsLogField(event.errors)
1378
- }, "graphql subscription event error");
1579
+ if (event.errors?.length) {
1580
+ log?.error({
1581
+ ...logFields,
1582
+ [FIELD_EVENT_COUNT]: eventCount,
1583
+ ...errorsLogField(event.errors)
1584
+ }, "graphql subscription event error");
1585
+ recordEventErrors(eventCount, event.errors);
1586
+ }
1379
1587
  yield event;
1380
1588
  }
1381
- log.info({
1382
- ...operationLogFields(operationName, operationType),
1383
- event_count: eventCount
1589
+ log?.info({
1590
+ ...logFields,
1591
+ [FIELD_EVENT_COUNT]: eventCount
1384
1592
  }, "graphql subscription completed");
1385
1593
  } catch (error) {
1386
- log.error({
1387
- ...operationLogFields(operationName, operationType),
1388
- event_count: eventCount,
1594
+ log?.error({
1595
+ ...logFields,
1596
+ [FIELD_EVENT_COUNT]: eventCount,
1389
1597
  ...errorLogField(error)
1390
1598
  }, "graphql subscription threw");
1599
+ recordEventErrors(eventCount, [error]);
1391
1600
  throw error;
1392
1601
  }
1393
1602
  }
1394
- function buildTracedExecute(executeFn, rumbleInput) {
1603
+ function buildTracedExecute(executeFn, rumbleInput, transport) {
1604
+ if (!telemetryEnabled(rumbleInput)) return executeFn;
1395
1605
  return async (options) => {
1396
- let log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
1397
- const operationName = options.operationName ?? "anonymous";
1398
- const operationType = getOperationType(options);
1399
- const start = Date.now();
1400
- log?.info(operationLogFields(operationName, operationType), "graphql execute start");
1401
- const run = async () => {
1402
- const result = await executeFn(options);
1403
- if (result && "errors" in result && result.errors?.length) log?.error({
1404
- ...operationLogFields(operationName, operationType),
1405
- duration_ms: Date.now() - start,
1406
- ...errorsLogField(result.errors)
1407
- }, "graphql execute completed with errors");
1408
- else log?.info({
1409
- ...operationLogFields(operationName, operationType),
1410
- duration_ms: Date.now() - start
1411
- }, "graphql execute completed");
1412
- return result;
1413
- };
1414
- if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(SpanNames.EXECUTE, { attributes: {
1415
- [AttributeNames.OPERATION_NAME]: operationName,
1416
- ...operationType ? { [AttributeNames.OPERATION_TYPE]: operationType } : {},
1417
- "graphql.document": print(options.document)
1418
- } }, async (span) => {
1419
- if (log && rumbleInput.logger?.injectTraceId !== false) log = log.child(traceIdLogFields(span));
1606
+ const tracer = telemetryTracer(rumbleInput);
1607
+ let log = telemetryLogger(rumbleInput);
1608
+ const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
1609
+ const start = startTimer();
1610
+ const run = async (span) => {
1611
+ log?.info({
1612
+ ...telemetry.logFields,
1613
+ ...telemetry.variableLogFields
1614
+ }, "graphql execute start");
1420
1615
  try {
1421
- const result = await run();
1616
+ const result = await executeFn(options);
1422
1617
  if (result && "errors" in result && result.errors?.length) {
1423
- for (const error of result.errors) span.recordException(error);
1424
- span.setStatus({ code: SpanStatusCode.ERROR });
1425
- }
1618
+ log?.error({
1619
+ ...telemetry.logFields,
1620
+ [FIELD_DURATION_MS]: durationMs(start),
1621
+ ...errorsLogField(result.errors)
1622
+ }, "graphql execute completed with errors");
1623
+ recordSpanErrors(span, result.errors);
1624
+ markErrorsReported(result.errors);
1625
+ } else log?.info({
1626
+ ...telemetry.logFields,
1627
+ [FIELD_DURATION_MS]: durationMs(start)
1628
+ }, "graphql execute completed");
1426
1629
  return result;
1427
1630
  } catch (error) {
1428
- if (error instanceof Error) span.recordException(error);
1429
1631
  log?.error({
1430
- ...operationLogFields(operationName, operationType),
1431
- duration_ms: Date.now() - start,
1632
+ ...telemetry.logFields,
1633
+ [FIELD_DURATION_MS]: durationMs(start),
1432
1634
  ...errorLogField(error)
1433
1635
  }, "graphql execute threw");
1434
- span.setStatus({ code: SpanStatusCode.ERROR });
1636
+ recordSpanError(span, error);
1637
+ markErrorsReported([error]);
1435
1638
  throw error;
1639
+ }
1640
+ };
1641
+ if (!tracer) return run();
1642
+ return tracer.startActiveSpan(SPAN_EXECUTE, { attributes: telemetry.attributes() }, async (span) => {
1643
+ if (log) {
1644
+ const correlation = traceCorrelationFields(rumbleInput, span);
1645
+ if (Object.keys(correlation).length > 0) log = log.child(correlation);
1646
+ }
1647
+ try {
1648
+ return await run(span);
1436
1649
  } finally {
1437
1650
  span.end();
1438
1651
  }
1439
1652
  });
1440
- try {
1441
- return await run();
1442
- } catch (error) {
1443
- log?.error({
1444
- ...operationLogFields(operationName, operationType),
1445
- duration_ms: Date.now() - start,
1446
- ...errorLogField(error)
1447
- }, "graphql execute threw");
1448
- throw error;
1449
- }
1450
1653
  };
1451
1654
  }
1452
- function buildTracedSubscribe(subscribeFn, rumbleInput) {
1655
+ function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
1656
+ if (!telemetryEnabled(rumbleInput)) return subscribeFn;
1453
1657
  return async (options) => {
1454
- let log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
1455
- const operationName = options.operationName ?? "anonymous";
1456
- const operationType = getOperationType(options);
1457
- const start = Date.now();
1458
- const doSubscribe = async () => {
1459
- log?.info(operationLogFields(operationName, operationType), "graphql subscribe start");
1460
- const result = await subscribeFn(options);
1461
- if (!isAsyncIterable(result)) {
1462
- const execResult = result;
1463
- if (execResult.errors?.length) log?.error({
1464
- ...operationLogFields(operationName, operationType),
1465
- duration_ms: Date.now() - start,
1466
- ...errorsLogField(execResult.errors)
1467
- }, "graphql subscribe completed with errors");
1468
- return execResult;
1469
- }
1658
+ const tracer = telemetryTracer(rumbleInput);
1659
+ let log = telemetryLogger(rumbleInput);
1660
+ const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
1661
+ const start = startTimer();
1662
+ const attributes = tracer ? telemetry.attributes() : {};
1663
+ const doSubscribe = async (span) => {
1470
1664
  log?.info({
1471
- ...operationLogFields(operationName, operationType),
1472
- duration_ms: Date.now() - start
1473
- }, "graphql subscription established");
1474
- if (log) return wrapSubscriptionIterator(result, log, operationName, operationType);
1475
- return result;
1476
- };
1477
- if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(SUBSCRIBE_SPAN_NAME, { attributes: {
1478
- [AttributeNames.OPERATION_NAME]: operationName,
1479
- ...operationType ? { [AttributeNames.OPERATION_TYPE]: operationType } : {},
1480
- "graphql.document": print(options.document)
1481
- } }, async (span) => {
1482
- if (log && rumbleInput.logger?.injectTraceId !== false) log = log.child(traceIdLogFields(span));
1665
+ ...telemetry.logFields,
1666
+ ...telemetry.variableLogFields
1667
+ }, "graphql subscribe start");
1483
1668
  try {
1484
- const result = await doSubscribe();
1669
+ const result = await subscribeFn(options);
1485
1670
  if (!isAsyncIterable(result)) {
1486
1671
  const execResult = result;
1487
1672
  if (execResult.errors?.length) {
1488
- for (const error of execResult.errors) span.recordException(error);
1489
- span.setStatus({ code: SpanStatusCode.ERROR });
1673
+ log?.error({
1674
+ ...telemetry.logFields,
1675
+ [FIELD_DURATION_MS]: durationMs(start),
1676
+ ...errorsLogField(execResult.errors)
1677
+ }, "graphql subscribe completed with errors");
1678
+ recordSpanErrors(span, execResult.errors);
1679
+ markErrorsReported(execResult.errors);
1490
1680
  }
1681
+ return execResult;
1491
1682
  }
1492
- return result;
1683
+ log?.info({
1684
+ ...telemetry.logFields,
1685
+ [FIELD_DURATION_MS]: durationMs(start)
1686
+ }, "graphql subscription established");
1687
+ return wrapSubscriptionIterator(result, {
1688
+ log,
1689
+ tracer,
1690
+ setupSpanContext: span?.spanContext(),
1691
+ logFields: telemetry.logFields,
1692
+ attributes
1693
+ });
1493
1694
  } catch (error) {
1494
- if (error instanceof Error) span.recordException(error);
1495
1695
  log?.error({
1496
- ...operationLogFields(operationName, operationType),
1497
- duration_ms: Date.now() - start,
1696
+ ...telemetry.logFields,
1697
+ [FIELD_DURATION_MS]: durationMs(start),
1498
1698
  ...errorLogField(error)
1499
1699
  }, "graphql subscribe threw");
1500
- span.setStatus({ code: SpanStatusCode.ERROR });
1700
+ recordSpanError(span, error);
1701
+ markErrorsReported([error]);
1501
1702
  throw error;
1703
+ }
1704
+ };
1705
+ if (!tracer) return doSubscribe();
1706
+ return tracer.startActiveSpan(SPAN_SUBSCRIBE, { attributes }, async (span) => {
1707
+ if (log) {
1708
+ const correlation = traceCorrelationFields(rumbleInput, span);
1709
+ if (Object.keys(correlation).length > 0) log = log.child(correlation);
1710
+ }
1711
+ try {
1712
+ return await doSubscribe(span);
1502
1713
  } finally {
1503
1714
  span.end();
1504
1715
  }
1505
1716
  });
1506
- try {
1507
- return await doSubscribe();
1508
- } catch (error) {
1509
- log?.error({
1510
- ...operationLogFields(operationName, operationType),
1511
- duration_ms: Date.now() - start,
1512
- ...errorLogField(error)
1513
- }, "graphql subscribe threw");
1514
- throw error;
1515
- }
1516
1717
  };
1517
1718
  }
1518
1719
  //#endregion
@@ -1661,17 +1862,30 @@ const isProbablyAConfigObject = (t) => {
1661
1862
  ].find((e) => e === k))) return true;
1662
1863
  return false;
1663
1864
  };
1664
- const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1865
+ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1866
+ const telemetryConfig = {
1867
+ otel,
1868
+ logger: loggerConfig
1869
+ };
1870
+ /**
1871
+ * Schema construction warnings go through the configured logger when there is
1872
+ * one, so they end up in the same structured stream as everything else, and
1873
+ * fall back to the console otherwise.
1874
+ */
1875
+ const warn = (msg, fields = {}) => {
1876
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1877
+ if (log) log.warn({
1878
+ ...fields,
1879
+ ...traceCorrelationFields(telemetryConfig)
1880
+ }, msg);
1881
+ else console.warn(msg);
1882
+ };
1665
1883
  return ({ table, refName, readAction = "read", adjust }) => {
1666
1884
  const tableSchema = tableHelper({
1667
1885
  db,
1668
1886
  table
1669
1887
  });
1670
- if (Object.keys(tableSchema.primaryKey).length === 0) {
1671
- const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1672
- const msg = `Could not find primary key for ${String(table)}. Cannot register subscriptions!`;
1673
- log ? log.warn({ "rumble.table": String(table) }, msg) : console.warn(msg);
1674
- }
1888
+ if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
1675
1889
  const primaryKey = Object.values(tableSchema.primaryKey)[0];
1676
1890
  const { registerOnInstance } = makePubSubInstance({ table });
1677
1891
  return schemaBuilder.drizzleObject(table, {
@@ -1680,9 +1894,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
1680
1894
  if (!primaryKey) return;
1681
1895
  const primaryKeyValue = element[primaryKey.name];
1682
1896
  if (!primaryKeyValue) {
1683
- const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1684
- const msg = `Could not find primary key value for element on ${String(table)}. Cannot register subscription!`;
1685
- log ? log.warn({ "rumble.table": String(table) }, msg) : console.warn(msg);
1897
+ warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
1686
1898
  return;
1687
1899
  }
1688
1900
  registerOnInstance({
@@ -1714,7 +1926,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
1714
1926
  } })) ?? {};
1715
1927
  const fields = Object.entries(columns).reduce((acc, [key, column]) => {
1716
1928
  if (key.startsWith("__")) {
1717
- console.warn(`Skipping column "${key}" on table "${tableSchema.tsName}": names starting with "__" are reserved by GraphQL introspection. Use a custom adjust() call if you really need to expose this column.`);
1929
+ warn(`Skipping column "${key}" on table "${tableSchema.tsName}": names starting with "__" are reserved by GraphQL introspection. Use a custom adjust() call if you really need to expose this column.`, { [ATTR_TABLE]: tableSchema.tsName });
1718
1930
  return acc;
1719
1931
  }
1720
1932
  if (userAdjustments[key]) {
@@ -1917,8 +2129,12 @@ const createPubSubInstance = ({ subscriptions }) => {
1917
2129
  };
1918
2130
  //#endregion
1919
2131
  //#region lib/query.ts
1920
- const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
2132
+ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1921
2133
  const registeredQueryFieldNames = /* @__PURE__ */ new Set();
2134
+ const telemetryConfig = {
2135
+ otel,
2136
+ logger: loggerConfig
2137
+ };
1922
2138
  return ({ table, readAction = "read", listAction = "read", findFirst, findMany }) => {
1923
2139
  const WhereArg = whereArgImplementer({ table });
1924
2140
  const OrderArg = orderArgImplementer({ table });
@@ -1990,7 +2206,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfi
1990
2206
  else {
1991
2207
  const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
1992
2208
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1993
- log ? log.info({}, msg) : console.info(msg);
2209
+ log ? log.info(traceCorrelationFields(telemetryConfig), msg) : console.info(msg);
1994
2210
  }
1995
2211
  return run(tx);
1996
2212
  });
@@ -2282,6 +2498,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2282
2498
  tracer;
2283
2499
  tracerEnabled;
2284
2500
  logger;
2501
+ telemetryConfig = {};
2285
2502
  filterLoaders = /* @__PURE__ */ new WeakMap();
2286
2503
  getLoader(context, filter) {
2287
2504
  let perContext = this.filterLoaders.get(context);
@@ -2310,6 +2527,10 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2310
2527
  this.tracer = this.builder.options.otel?.tracer;
2311
2528
  this.tracerEnabled = this.builder.options.otel?.enabled;
2312
2529
  this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
2530
+ this.telemetryConfig = {
2531
+ otel: this.builder.options.otel,
2532
+ logger: this.builder.options.logger
2533
+ };
2313
2534
  return typeConfig;
2314
2535
  }
2315
2536
  wrapResolve(resolver, fieldConfig) {
@@ -2321,13 +2542,16 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2321
2542
  if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
2322
2543
  const runFilters = async (span) => {
2323
2544
  const allFilters = Array.isArray(filters) ? filters : [filters];
2324
- span?.setAttribute("filters.total", allFilters.length);
2545
+ span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
2325
2546
  const loaders = allFilters.map((filter) => this.getLoader(context, filter));
2326
2547
  let resolved;
2327
- if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(`rumble.filter.resolve`, async (span) => {
2328
- span.setAttribute("graphql.field.name", fieldConfig.name);
2548
+ if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(SPAN_FILTERS_RESOLVE, async (span) => {
2549
+ span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
2329
2550
  try {
2330
2551
  return await resolver(parent, args, context, info);
2552
+ } catch (error) {
2553
+ recordSpanError(span, error);
2554
+ throw error;
2331
2555
  } finally {
2332
2556
  span.end();
2333
2557
  }
@@ -2336,24 +2560,30 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2336
2560
  const entities = Array.isArray(resolved) ? resolved : [resolved];
2337
2561
  const perFilterResults = await Promise.all(loaders.map((loader) => Promise.all(entities.map((entity) => loader.load(entity)))));
2338
2562
  const allowed = entities.filter((_, index) => perFilterResults.some((results) => results[index] != null));
2339
- span?.setAttribute("filters.allowed", allowed.length);
2563
+ span?.setAttribute(ATTR_FILTERS_ALLOWED, allowed.length);
2340
2564
  this.logger?.debug({
2341
- "graphql.field.name": fieldConfig.name,
2342
- "filters.total": allFilters.length,
2343
- "filters.allowed": allowed.length
2565
+ [ATTR_FIELD_NAME]: fieldConfig.name,
2566
+ [ATTR_FILTERS_TOTAL]: allFilters.length,
2567
+ [ATTR_FILTERS_ALLOWED]: allowed.length,
2568
+ ...traceCorrelationFields(this.telemetryConfig)
2344
2569
  }, "runtime filters applied");
2345
2570
  if (Array.isArray(resolved)) return allowed;
2346
2571
  return allowed[0] ?? null;
2347
2572
  };
2348
- if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`rumble.filter.apply`, async (span) => {
2349
- span.setAttribute("graphql.field.name", fieldConfig.name);
2573
+ const logFilterError = (error) => {
2574
+ this.logger?.error({
2575
+ [ATTR_FIELD_NAME]: fieldConfig.name,
2576
+ ...traceCorrelationFields(this.telemetryConfig),
2577
+ ...errorLogField(error)
2578
+ }, "runtime filter threw");
2579
+ };
2580
+ if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(SPAN_FILTERS_APPLY, async (span) => {
2581
+ span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
2350
2582
  try {
2351
2583
  return await runFilters(span);
2352
2584
  } catch (error) {
2353
- this.logger?.error({
2354
- "graphql.field.name": fieldConfig.name,
2355
- ...errorLogField(error)
2356
- }, "runtime filter threw");
2585
+ logFilterError(error);
2586
+ recordSpanError(span, error);
2357
2587
  throw error;
2358
2588
  } finally {
2359
2589
  span.end();
@@ -2362,10 +2592,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2362
2592
  else try {
2363
2593
  return await runFilters();
2364
2594
  } catch (error) {
2365
- this.logger?.error({
2366
- "graphql.field.name": fieldConfig.name,
2367
- ...errorLogField(error)
2368
- }, "runtime filter threw");
2595
+ logFilterError(error);
2369
2596
  throw error;
2370
2597
  }
2371
2598
  };
@@ -2413,18 +2640,22 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2413
2640
  let r = createSpan ? createSpan(resolver, options) : resolver;
2414
2641
  if (logger?.enabled) {
2415
2642
  const log = logger.logger;
2643
+ const telemetryConfig = {
2644
+ otel,
2645
+ logger
2646
+ };
2416
2647
  r = wrapResolver(r, (error, duration) => {
2648
+ const fields = {
2649
+ [ATTR_FIELD_NAME]: config.name,
2650
+ [ATTR_PARENT_TYPE]: config.parentType,
2651
+ [FIELD_DURATION_MS]: duration,
2652
+ ...traceCorrelationFields(telemetryConfig)
2653
+ };
2417
2654
  if (error) log.error({
2418
- "graphql.field.name": config.name,
2419
- "graphql.parent.type": config.parentType,
2420
- duration_ms: duration,
2655
+ ...fields,
2421
2656
  ...errorLogField(error)
2422
2657
  }, "resolver failed");
2423
- else log.debug({
2424
- "graphql.field.name": config.name,
2425
- "graphql.parent.type": config.parentType,
2426
- duration_ms: duration
2427
- }, "resolver completed");
2658
+ else log.debug(fields, "resolver completed");
2428
2659
  });
2429
2660
  }
2430
2661
  return r;
@@ -2446,6 +2677,27 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2446
2677
  };
2447
2678
  //#endregion
2448
2679
  //#region lib/rumble.ts
2680
+ /**
2681
+ * Mirrors sofa's own default error handler: it derives the HTTP status from
2682
+ * `extensions.http` and returns the errors as JSON. rumble only adds telemetry
2683
+ * around error handling, so a request that fails must still answer with the
2684
+ * status and body a plain sofa setup would have produced.
2685
+ */
2686
+ function sofaErrorResponse(errors) {
2687
+ let status;
2688
+ const headers = { "Content-Type": "application/json; charset=utf-8" };
2689
+ for (const error of errors) {
2690
+ const http = error?.extensions?.http;
2691
+ if (!http) continue;
2692
+ if (http.status && (!status || http.status > status)) status = http.status;
2693
+ if (http.headers) Object.assign(headers, http.headers);
2694
+ delete error.extensions.http;
2695
+ }
2696
+ return Response.json({ errors }, {
2697
+ status: status ?? 500,
2698
+ headers
2699
+ });
2700
+ }
2449
2701
  const rumble = (rumbleInput) => {
2450
2702
  if (!rumbleInput.db._.relations) throw new RumbleError(`
2451
2703
  rumble could not find any relations in the provided drizzle instance.
@@ -2551,6 +2803,33 @@ export const r = rumble({
2551
2803
  types: filteredTypes
2552
2804
  });
2553
2805
  });
2806
+ /**
2807
+ * Adapts the configured rumble logger to the logger interface yoga expects,
2808
+ * so yoga's own diagnostics end up in the same structured stream (with the
2809
+ * same `rumble.transport` and trace correlation fields) as rumble's entries.
2810
+ */
2811
+ const yogaLogging = () => {
2812
+ const log = telemetryLogger(rumbleInput);
2813
+ if (!log) return void 0;
2814
+ const emit = (level) => (...args) => {
2815
+ const messages = args.filter((arg) => typeof arg === "string");
2816
+ const rest = args.filter((arg) => typeof arg !== "string");
2817
+ const errors = rest.filter((entry) => entry instanceof Error);
2818
+ const details = rest.filter((entry) => !(entry instanceof Error));
2819
+ log[level]({
2820
+ [ATTR_TRANSPORT]: "graphql",
2821
+ ...traceCorrelationFields(rumbleInput),
2822
+ ...errors.length > 0 ? errorsLogField(errors) : {},
2823
+ ...details.length > 0 ? { details } : {}
2824
+ }, messages.join(" ") || "yoga diagnostic");
2825
+ };
2826
+ return {
2827
+ debug: emit("debug"),
2828
+ info: emit("info"),
2829
+ warn: emit("warn"),
2830
+ error: emit("error")
2831
+ };
2832
+ };
2554
2833
  const createYoga$1 = (args) => {
2555
2834
  const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
2556
2835
  return createYoga({
@@ -2558,38 +2837,41 @@ export const r = rumble({
2558
2837
  graphiql: enableApiDocs,
2559
2838
  schema: builtSchema(),
2560
2839
  context,
2840
+ logging: args?.logging ?? yogaLogging() ?? true,
2561
2841
  plugins: [
2562
2842
  ...args?.plugins ?? [],
2563
2843
  ...enableApiDocs ? [] : [useDisableIntrospection(), EnvelopArmorPlugin(args?.armorConfig)],
2564
- rumbleInput.otel?.enabled || rumbleInput.logger?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
2565
- setExecuteFn(buildTracedExecute(executeFn, rumbleInput));
2566
- } } : false
2844
+ telemetryEnabled(rumbleInput) ? {
2845
+ onExecute: ({ setExecuteFn, executeFn }) => {
2846
+ setExecuteFn(buildTracedExecute(executeFn, rumbleInput, "graphql"));
2847
+ },
2848
+ onSubscribe: ({ setSubscribeFn, subscribeFn }) => {
2849
+ setSubscribeFn(buildTracedSubscribe(subscribeFn, rumbleInput, "graphql"));
2850
+ }
2851
+ } : false
2567
2852
  ].filter(Boolean)
2568
2853
  });
2569
2854
  };
2570
2855
  const createSofa = async (args) => {
2571
2856
  const { useSofa: useSofaFn } = await import("@m1212e/sofa-api");
2572
2857
  if (args.openAPI) merge(args.openAPI, sofaOpenAPIWebhookDocs);
2573
- if (args.errorHandler) {
2574
- const originalHandler = args.errorHandler;
2575
- args.errorHandler = (errors) => {
2576
- const span = trace.getActiveSpan();
2577
- for (const error of errors) span?.recordException(error);
2578
- span?.setStatus({ code: SpanStatusCode.ERROR });
2579
- return originalHandler(errors);
2580
- };
2581
- }
2858
+ const userErrorHandler = args.errorHandler;
2582
2859
  return useSofaFn({
2583
2860
  ...args,
2584
2861
  schema: builtSchema(),
2585
2862
  context,
2586
- execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput),
2587
- subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput),
2863
+ execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput, "rest"),
2864
+ subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput, "rest"),
2588
2865
  errorHandler(errors) {
2589
- const span = trace.getActiveSpan();
2590
- for (const error of errors) span?.recordException(error);
2591
- span?.setStatus({ code: SpanStatusCode.ERROR });
2592
- return new Response(errors[0].message, { status: 500 });
2866
+ if (!errorsAlreadyReported(errors)) {
2867
+ recordSpanErrors(trace.getActiveSpan(), errors);
2868
+ telemetryLogger(rumbleInput)?.error({
2869
+ [ATTR_TRANSPORT]: "rest",
2870
+ ...traceCorrelationFields(rumbleInput),
2871
+ ...errorsLogField(errors)
2872
+ }, "rest request failed");
2873
+ }
2874
+ return userErrorHandler?.(errors) ?? sofaErrorResponse(errors);
2593
2875
  }
2594
2876
  });
2595
2877
  };
@@ -2598,8 +2880,8 @@ export const r = rumble({
2598
2880
  ...args,
2599
2881
  schema: builtSchema(),
2600
2882
  context,
2601
- execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput),
2602
- subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput)
2883
+ execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput, "ws"),
2884
+ subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput, "ws")
2603
2885
  }, ...rest);
2604
2886
  };
2605
2887
  return {