@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.cjs CHANGED
@@ -30,12 +30,13 @@ let _opentelemetry_api = require("@opentelemetry/api");
30
30
  let es_toolkit = require("es-toolkit");
31
31
  let graphql_yoga = require("graphql-yoga");
32
32
  let drizzle_orm = require("drizzle-orm");
33
+ let _opentelemetry_semantic_conventions_incubating = require("@opentelemetry/semantic-conventions/incubating");
34
+ let _pothos_tracing_opentelemetry = require("@pothos/tracing-opentelemetry");
33
35
  let drizzle_orm_casing = require("drizzle-orm/casing");
34
36
  let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
35
37
  let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
36
38
  let pluralize = require("pluralize");
37
39
  pluralize = __toESM(pluralize, 1);
38
- let _pothos_tracing_opentelemetry = require("@pothos/tracing-opentelemetry");
39
40
  let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
40
41
  let _pothos_core = require("@pothos/core");
41
42
  _pothos_core = __toESM(_pothos_core, 1);
@@ -210,7 +211,39 @@ function mapNullFieldsToUndefined(obj) {
210
211
  }
211
212
  //#endregion
212
213
  //#region package.json
213
- var version = "0.23.17";
214
+ var version = "0.23.19";
215
+ //#endregion
216
+ //#region lib/helpers/errorLogging.ts
217
+ function hasMessage(error) {
218
+ return typeof error === "object" && error !== null && "message" in error;
219
+ }
220
+ function toLoggedException(error) {
221
+ if (hasMessage(error)) {
222
+ const gqlError = error;
223
+ return {
224
+ "exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
225
+ "exception.message": String(gqlError.message),
226
+ ...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
227
+ ...gqlError.path ? { path: gqlError.path } : {},
228
+ ...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
229
+ };
230
+ }
231
+ return {
232
+ "exception.type": "UnknownError",
233
+ "exception.message": String(error)
234
+ };
235
+ }
236
+ /**
237
+ * Every erroneous rumble log carries the same `errors` array shape,
238
+ * whether it originates from a thrown exception (single-element array)
239
+ * or a GraphQL `ExecutionResult.errors` list (multi-element array).
240
+ */
241
+ function errorsLogField(errors) {
242
+ return { errors: errors.map(toLoggedException) };
243
+ }
244
+ function errorLogField(error) {
245
+ return errorsLogField([error]);
246
+ }
214
247
  //#endregion
215
248
  //#region lib/helpers/mergeFilters.ts
216
249
  const EmptyFilter$1 = Symbol.for("drizzle:EmptyFilter");
@@ -470,6 +503,182 @@ function tableHelper({ db, table }) {
470
503
  };
471
504
  }
472
505
  //#endregion
506
+ //#region lib/helpers/telemetry.ts
507
+ const SPAN_EXECUTE = _pothos_tracing_opentelemetry.SpanNames.EXECUTE;
508
+ /** @pothos/tracing-opentelemetry's SpanNames enum has no subscribe member. */
509
+ const SPAN_SUBSCRIBE = "graphql.subscribe";
510
+ const SPAN_SUBSCRIBE_EVENT = "graphql.subscribe.event";
511
+ const SPAN_ABILITIES_PREPARE = "rumble.abilities.prepare";
512
+ const SPAN_FILTERS_APPLY = "rumble.filters.apply";
513
+ const SPAN_FILTERS_RESOLVE = "rumble.filters.resolve";
514
+ const ATTR_OPERATION_NAME = _opentelemetry_semantic_conventions_incubating.ATTR_GRAPHQL_OPERATION_NAME;
515
+ const ATTR_OPERATION_TYPE = _opentelemetry_semantic_conventions_incubating.ATTR_GRAPHQL_OPERATION_TYPE;
516
+ const ATTR_DOCUMENT = _opentelemetry_semantic_conventions_incubating.ATTR_GRAPHQL_DOCUMENT;
517
+ const ATTR_FIELD_NAME = _pothos_tracing_opentelemetry.AttributeNames.FIELD_NAME;
518
+ /** Prefix for per-variable span attributes, e.g. `graphql.variables.userId`. */
519
+ const ATTR_VARIABLES_PREFIX = _pothos_tracing_opentelemetry.AttributeNames.VARIABLES;
520
+ /** Log field holding all variables as one object (logs keep nesting, spans flatten). */
521
+ const FIELD_VARIABLES = "graphql.variables";
522
+ const ATTR_PARENT_TYPE = "graphql.parent.type";
523
+ const ATTR_TRANSPORT = "rumble.transport";
524
+ const ATTR_TABLE = "rumble.table";
525
+ const ATTR_ACTION = "rumble.action";
526
+ const ATTR_ABILITIES_STATUS = "rumble.abilities.status";
527
+ const ATTR_ABILITIES_STATIC = "rumble.abilities.static";
528
+ const ATTR_ABILITIES_DYNAMIC = "rumble.abilities.dynamic";
529
+ const ATTR_ABILITIES_TOTAL = "rumble.abilities.total";
530
+ const ATTR_FILTERS_TOTAL = "rumble.filters.total";
531
+ const ATTR_FILTERS_ALLOWED = "rumble.filters.allowed";
532
+ const ATTR_SUBSCRIPTION_EVENT_INDEX = "rumble.subscription.event_index";
533
+ const FIELD_DURATION_MS = "duration_ms";
534
+ const FIELD_EVENT_COUNT = "event_count";
535
+ function telemetryLogger(config) {
536
+ return config.logger?.enabled ? config.logger.logger : void 0;
537
+ }
538
+ function telemetryTracer(config) {
539
+ return config.otel?.enabled ? config.otel.tracer : void 0;
540
+ }
541
+ /**
542
+ * Whether anything at all should be instrumented. When this is false, rumble
543
+ * hands the untouched execute/subscribe functions back to the transports so a
544
+ * user who wants neither traces nor logs pays no overhead.
545
+ */
546
+ function telemetryEnabled(config) {
547
+ return Boolean(config.otel?.enabled || config.logger?.enabled);
548
+ }
549
+ /**
550
+ * `trace_id`/`span_id`/`trace_flags` for the given span, or for the currently
551
+ * active one when no span is passed. Field names and the two-hex-digit flags
552
+ * format follow the OpenTelemetry log data model, which is what log backends
553
+ * (Loki, Tempo, Elastic) expect when linking a log line to its trace.
554
+ *
555
+ * Reading the *active* span means every rumble log site gets correlation for
556
+ * free, without threading a child logger through the whole library.
557
+ */
558
+ function traceCorrelationFields(config, span) {
559
+ if (!config.otel?.enabled || config.logger?.injectTraceId === false) return {};
560
+ const target = span ?? _opentelemetry_api.trace.getActiveSpan();
561
+ if (!target) return {};
562
+ const spanContext = target.spanContext();
563
+ if (!(0, _opentelemetry_api.isSpanContextValid)(spanContext)) return {};
564
+ return {
565
+ trace_id: spanContext.traceId,
566
+ span_id: spanContext.spanId,
567
+ trace_flags: spanContext.traceFlags.toString(16).padStart(2, "0")
568
+ };
569
+ }
570
+ function toException(error) {
571
+ if (error instanceof Error) return error;
572
+ if (typeof error === "object" && error !== null && "message" in error) return error;
573
+ return String(error);
574
+ }
575
+ function exceptionMessage(error) {
576
+ if (typeof error === "object" && error !== null && "message" in error) return String(error.message);
577
+ return error === void 0 ? void 0 : String(error);
578
+ }
579
+ /**
580
+ * Records every error as an exception event and flips the span to ERROR, per
581
+ * OpenTelemetry's exception conventions. Used for rumble's own spans as well as
582
+ * the GraphQL operation spans so a failure looks identical wherever it happens.
583
+ */
584
+ function recordSpanErrors(span, errors) {
585
+ if (!span || errors.length === 0) return;
586
+ for (const error of errors) span.recordException(toException(error));
587
+ span.setStatus({
588
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
589
+ message: exceptionMessage(errors[0])
590
+ });
591
+ }
592
+ function recordSpanError(span, error) {
593
+ recordSpanErrors(span, [error]);
594
+ }
595
+ /**
596
+ * Errors the operation wrapper has already reported.
597
+ *
598
+ * A transport can see the same failure twice: sofa, for instance, hands
599
+ * `result.errors` to its error handler *after* execute has returned, so without
600
+ * this the very same error would produce two log lines and two exception events,
601
+ * which quietly doubles error counts in dashboards. A WeakSet keeps this free of
602
+ * bookkeeping — entries disappear with the errors themselves.
603
+ */
604
+ const reportedErrors = /* @__PURE__ */ new WeakSet();
605
+ function markErrorsReported(errors) {
606
+ for (const error of errors) if (typeof error === "object" && error !== null) reportedErrors.add(error);
607
+ }
608
+ /** True when every error was already reported by the operation wrapper. */
609
+ function errorsAlreadyReported(errors) {
610
+ if (errors.length === 0) return false;
611
+ return errors.every((error) => typeof error === "object" && error !== null && reportedErrors.has(error));
612
+ }
613
+ function safeJsonStringify(value) {
614
+ try {
615
+ return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner) ?? "null";
616
+ } catch {
617
+ return String(value);
618
+ }
619
+ }
620
+ function isHomogeneousPrimitiveArray(value) {
621
+ let seen;
622
+ for (const entry of value) {
623
+ if (entry === null || entry === void 0) continue;
624
+ const type = typeof entry;
625
+ if (type !== "string" && type !== "number" && type !== "boolean") return false;
626
+ if (seen && seen !== type) return false;
627
+ seen = type;
628
+ }
629
+ return true;
630
+ }
631
+ /**
632
+ * Coerces an arbitrary value into something the OpenTelemetry attribute type
633
+ * allows: primitives pass through, homogeneous primitive arrays stay arrays
634
+ * (mixed ones are invalid per spec) and everything else becomes JSON.
635
+ *
636
+ * Values are intentionally not truncated here — length limits belong to the SDK
637
+ * (`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`) so they stay configurable per deployment.
638
+ */
639
+ function attributeValue(value) {
640
+ switch (typeof value) {
641
+ case "string": return value;
642
+ case "number": return Number.isFinite(value) ? value : String(value);
643
+ case "boolean": return value;
644
+ case "bigint": return value.toString();
645
+ case "undefined": return "null";
646
+ default: break;
647
+ }
648
+ if (value === null) return "null";
649
+ if (Array.isArray(value) && isHomogeneousPrimitiveArray(value)) return value;
650
+ return safeJsonStringify(value);
651
+ }
652
+ function resolveVariables(variables, include) {
653
+ if (include === false || !variables) return void 0;
654
+ const resolved = typeof include === "function" ? include({ ...variables }) : variables;
655
+ if (!resolved || Object.keys(resolved).length === 0) return void 0;
656
+ return resolved;
657
+ }
658
+ /** Variables flattened into `graphql.variables.<name>` span attributes. */
659
+ function variableAttributes(variables, include) {
660
+ const resolved = resolveVariables(variables, include);
661
+ if (!resolved) return {};
662
+ const attributes = {};
663
+ for (const [key, value] of Object.entries(resolved)) attributes[`${ATTR_VARIABLES_PREFIX}${key}`] = attributeValue(value);
664
+ return attributes;
665
+ }
666
+ /** Variables as a single nested `graphql.variables` log field. */
667
+ function variableLogField(variables, include) {
668
+ const resolved = resolveVariables(variables, include);
669
+ return resolved ? { [FIELD_VARIABLES]: resolved } : {};
670
+ }
671
+ /**
672
+ * Monotonic clock reading. `Date.now()` can jump backwards when the wall clock is
673
+ * adjusted (NTP), which would produce negative or inflated durations.
674
+ */
675
+ function startTimer() {
676
+ return performance.now();
677
+ }
678
+ function durationMs(start) {
679
+ return Math.round((performance.now() - start) * 100) / 100;
680
+ }
681
+ //#endregion
473
682
  //#region lib/abilityBuilder.ts
474
683
  function isDynamicQueryFilter(filter) {
475
684
  return typeof filter === "function" && filter.constructor.name !== "AsyncFunction";
@@ -477,17 +686,22 @@ function isDynamicQueryFilter(filter) {
477
686
  function isStaticQueryFilter(filter) {
478
687
  return typeof filter !== "function";
479
688
  }
480
- const makeNothingRegisteredWarner = (logger) => (0, es_toolkit.debounce)((model, action) => {
689
+ const makeNothingRegisteredWarner = (config, logger) => (0, es_toolkit.debounce)((model, action) => {
481
690
  const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
482
691
  if (logger) logger.warn({
483
- "rumble.table": model,
484
- "rumble.action": action
692
+ [ATTR_TABLE]: model,
693
+ [ATTR_ACTION]: action,
694
+ ...traceCorrelationFields(config)
485
695
  }, msg);
486
696
  else console.warn(msg);
487
697
  }, 1e3);
488
698
  const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
489
699
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
490
- const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(log);
700
+ const telemetryConfig = {
701
+ otel,
702
+ logger: loggerConfig
703
+ };
704
+ const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
491
705
  const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
492
706
  db,
493
707
  table: name
@@ -747,14 +961,14 @@ by: (explicitFilter) => {
747
961
  }
748
962
  return { withContext: (userContext) => {
749
963
  return { filter: (action) => {
750
- const assembleAbilities = (span) => {
964
+ const assembleAbilities = (attributes) => {
751
965
  const filters = queryFilters.get(action);
752
966
  if (filters === "unrestricted") {
753
- span?.setAttribute("abilities.status", "unrestricted");
967
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
754
968
  return transformToResponse();
755
969
  }
756
970
  if (!filters) {
757
- span?.setAttribute("abilities.status", "blocked_everything");
971
+ attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
758
972
  nothingRegisteredWarningLogger(String(tableName), action);
759
973
  return transformToResponse(blockEverythingFilter);
760
974
  }
@@ -763,58 +977,60 @@ by: (explicitFilter) => {
763
977
  for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
764
978
  const func = dynamicQueryFilters[action][i];
765
979
  const result = func(userContext);
766
- if (result === "allow") return transformToResponse();
980
+ if (result === "allow") {
981
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
982
+ return transformToResponse();
983
+ }
767
984
  if (result === void 0) continue;
768
985
  dynamicResults[filtersReturned++] = result;
769
986
  }
770
987
  dynamicResults.length = filtersReturned;
771
- span?.setAttribute("abilities.dynamic", dynamicResults.length);
772
- span?.setAttribute("abilities.static", simpleQueryFilters[action].length);
988
+ attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
989
+ attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
773
990
  const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
774
- span?.setAttribute("abilities.total", allQueryFilters.length);
991
+ attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
775
992
  if (allQueryFilters.length === 0) {
776
- span?.setAttribute("abilities.status", "blocked_everything");
993
+ attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
777
994
  return transformToResponse(blockEverythingFilter);
778
995
  }
779
996
  const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
780
997
  return mergeFilters(a, b, "OR");
781
998
  });
782
- span?.setAttribute("abilities.status", "applied");
999
+ attributes[ATTR_ABILITIES_STATUS] = "applied";
783
1000
  return transformToResponse(mergedFilters);
784
1001
  };
785
- if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(`rumble.abilities.prepare`, (span) => {
786
- span.setAttribute("rumble.action", action);
787
- span.setAttribute("rumble.table", String(tableName));
1002
+ const run = (span) => {
1003
+ const attributes = {
1004
+ [ATTR_TABLE]: String(tableName),
1005
+ [ATTR_ACTION]: action
1006
+ };
788
1007
  try {
789
- const attrs = {
790
- "rumble.table": String(tableName),
791
- "rumble.action": action
792
- };
793
- const proxy = new Proxy(span, { get(target, prop) {
794
- if (prop === "setAttribute") return (k, v) => {
795
- attrs[k] = v;
796
- return target.setAttribute(k, v);
797
- };
798
- return target[prop];
799
- } });
800
- const result = assembleAbilities(proxy);
801
- log?.debug(attrs, "abilities prepared");
1008
+ const result = assembleAbilities(attributes);
1009
+ log?.debug({
1010
+ ...attributes,
1011
+ ...traceCorrelationFields(telemetryConfig, span)
1012
+ }, "abilities prepared");
802
1013
  return result;
1014
+ } catch (error) {
1015
+ recordSpanError(span, error);
1016
+ log?.error({
1017
+ ...attributes,
1018
+ ...traceCorrelationFields(telemetryConfig, span),
1019
+ ...errorLogField(error)
1020
+ }, "abilities failed");
1021
+ throw error;
1022
+ } finally {
1023
+ span?.setAttributes(attributes);
1024
+ }
1025
+ };
1026
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, (span) => {
1027
+ try {
1028
+ return run(span);
803
1029
  } finally {
804
1030
  span.end();
805
1031
  }
806
1032
  });
807
- else {
808
- const attrs = {
809
- "rumble.table": String(tableName),
810
- "rumble.action": action
811
- };
812
- const result = assembleAbilities(log ? { setAttribute: (k, v) => {
813
- attrs[k] = v;
814
- } } : void 0);
815
- log?.debug(attrs, "abilities prepared");
816
- return result;
817
- }
1033
+ return run();
818
1034
  } };
819
1035
  } };
820
1036
  };
@@ -1341,207 +1557,192 @@ const sofaOpenAPIWebhookDocs = {
1341
1557
  } }
1342
1558
  };
1343
1559
  //#endregion
1344
- //#region lib/helpers/errorLogging.ts
1345
- function hasMessage(error) {
1346
- return typeof error === "object" && error !== null && "message" in error;
1560
+ //#region lib/helpers/tracing.ts
1561
+ function isAsyncIterable(value) {
1562
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
1347
1563
  }
1348
- function toLoggedException(error) {
1349
- if (hasMessage(error)) {
1350
- const gqlError = error;
1351
- return {
1352
- "exception.type": gqlError.constructor?.name ?? gqlError.name ?? "Error",
1353
- "exception.message": String(gqlError.message),
1354
- ...gqlError.stack ? { "exception.stacktrace": gqlError.stack } : {},
1355
- ...gqlError.path ? { path: gqlError.path } : {},
1356
- ...gqlError.extensions && Object.keys(gqlError.extensions).length > 0 ? { extensions: gqlError.extensions } : {}
1357
- };
1358
- }
1564
+ function buildOperationTelemetry(config, transport, options) {
1565
+ const operationAst = (0, graphql.getOperationAST)(options.document, options.operationName);
1566
+ const operationName = options.operationName ?? operationAst?.name?.value ?? "anonymous";
1567
+ const operationType = operationAst?.operation;
1568
+ const logFields = {
1569
+ [ATTR_OPERATION_NAME]: operationName,
1570
+ ...operationType ? { [ATTR_OPERATION_TYPE]: operationType } : {},
1571
+ [ATTR_TRANSPORT]: transport
1572
+ };
1359
1573
  return {
1360
- "exception.type": "UnknownError",
1361
- "exception.message": String(error)
1574
+ operationName,
1575
+ operationType,
1576
+ logFields,
1577
+ variableLogFields: variableLogField(options.variableValues, config.logger?.includeVariables),
1578
+ attributes: () => ({
1579
+ ...logFields,
1580
+ ...config.otel?.includeDocument === false ? {} : { [ATTR_DOCUMENT]: (0, graphql.print)(options.document) },
1581
+ ...variableAttributes(options.variableValues, config.otel?.includeVariables)
1582
+ })
1362
1583
  };
1363
1584
  }
1364
1585
  /**
1365
- * Every erroneous rumble log carries the same `errors` array shape,
1366
- * whether it originates from a thrown exception (single-element array)
1367
- * or a GraphQL `ExecutionResult.errors` list (multi-element array).
1586
+ * Wraps a subscription's event stream so delivered events are observable no
1587
+ * matter which transport consumes them.
1368
1588
  */
1369
- function errorsLogField(errors) {
1370
- return { errors: errors.map(toLoggedException) };
1371
- }
1372
- function errorLogField(error) {
1373
- return errorsLogField([error]);
1374
- }
1375
- //#endregion
1376
- //#region lib/helpers/tracing.ts
1377
- const SUBSCRIBE_SPAN_NAME = "graphql.subscribe";
1378
- function getOperationType(options) {
1379
- return (0, graphql.getOperationAST)(options.document, options.operationName)?.operation;
1380
- }
1381
- function operationLogFields(operationName, operationType) {
1382
- return {
1383
- "graphql.operation.name": operationName,
1384
- ...operationType ? { "graphql.operation.type": operationType } : {}
1385
- };
1386
- }
1387
- function traceIdLogFields(span) {
1388
- const { traceId, spanId, traceFlags } = span.spanContext();
1389
- return {
1390
- trace_id: traceId,
1391
- span_id: spanId,
1392
- trace_flags: traceFlags.toString(16).padStart(2, "0")
1589
+ async function* wrapSubscriptionIterator(iterator, telemetry) {
1590
+ const { log, tracer, setupSpanContext, logFields, attributes } = telemetry;
1591
+ const recordEventErrors = (eventCount, errors) => {
1592
+ if (!tracer) return;
1593
+ const span = tracer.startSpan(SPAN_SUBSCRIBE_EVENT, {
1594
+ root: true,
1595
+ links: setupSpanContext ? [{ context: setupSpanContext }] : void 0,
1596
+ attributes: {
1597
+ ...attributes,
1598
+ [ATTR_SUBSCRIPTION_EVENT_INDEX]: eventCount
1599
+ }
1600
+ });
1601
+ recordSpanErrors(span, errors);
1602
+ span.end();
1393
1603
  };
1394
- }
1395
- function isAsyncIterable(value) {
1396
- return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
1397
- }
1398
- async function* wrapSubscriptionIterator(iterator, log, operationName, operationType) {
1399
1604
  let eventCount = 0;
1400
1605
  try {
1401
1606
  for await (const event of iterator) {
1402
1607
  eventCount++;
1403
- if (event.errors?.length) log.error({
1404
- ...operationLogFields(operationName, operationType),
1405
- event_count: eventCount,
1406
- ...errorsLogField(event.errors)
1407
- }, "graphql subscription event error");
1608
+ if (event.errors?.length) {
1609
+ log?.error({
1610
+ ...logFields,
1611
+ [FIELD_EVENT_COUNT]: eventCount,
1612
+ ...errorsLogField(event.errors)
1613
+ }, "graphql subscription event error");
1614
+ recordEventErrors(eventCount, event.errors);
1615
+ }
1408
1616
  yield event;
1409
1617
  }
1410
- log.info({
1411
- ...operationLogFields(operationName, operationType),
1412
- event_count: eventCount
1618
+ log?.info({
1619
+ ...logFields,
1620
+ [FIELD_EVENT_COUNT]: eventCount
1413
1621
  }, "graphql subscription completed");
1414
1622
  } catch (error) {
1415
- log.error({
1416
- ...operationLogFields(operationName, operationType),
1417
- event_count: eventCount,
1623
+ log?.error({
1624
+ ...logFields,
1625
+ [FIELD_EVENT_COUNT]: eventCount,
1418
1626
  ...errorLogField(error)
1419
1627
  }, "graphql subscription threw");
1628
+ recordEventErrors(eventCount, [error]);
1420
1629
  throw error;
1421
1630
  }
1422
1631
  }
1423
- function buildTracedExecute(executeFn, rumbleInput) {
1632
+ function buildTracedExecute(executeFn, rumbleInput, transport) {
1633
+ if (!telemetryEnabled(rumbleInput)) return executeFn;
1424
1634
  return async (options) => {
1425
- let log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
1426
- const operationName = options.operationName ?? "anonymous";
1427
- const operationType = getOperationType(options);
1428
- const start = Date.now();
1429
- log?.info(operationLogFields(operationName, operationType), "graphql execute start");
1430
- const run = async () => {
1431
- const result = await executeFn(options);
1432
- if (result && "errors" in result && result.errors?.length) log?.error({
1433
- ...operationLogFields(operationName, operationType),
1434
- duration_ms: Date.now() - start,
1435
- ...errorsLogField(result.errors)
1436
- }, "graphql execute completed with errors");
1437
- else log?.info({
1438
- ...operationLogFields(operationName, operationType),
1439
- duration_ms: Date.now() - start
1440
- }, "graphql execute completed");
1441
- return result;
1442
- };
1443
- if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(_pothos_tracing_opentelemetry.SpanNames.EXECUTE, { attributes: {
1444
- [_pothos_tracing_opentelemetry.AttributeNames.OPERATION_NAME]: operationName,
1445
- ...operationType ? { [_pothos_tracing_opentelemetry.AttributeNames.OPERATION_TYPE]: operationType } : {},
1446
- "graphql.document": (0, graphql.print)(options.document)
1447
- } }, async (span) => {
1448
- if (log && rumbleInput.logger?.injectTraceId !== false) log = log.child(traceIdLogFields(span));
1635
+ const tracer = telemetryTracer(rumbleInput);
1636
+ let log = telemetryLogger(rumbleInput);
1637
+ const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
1638
+ const start = startTimer();
1639
+ const run = async (span) => {
1640
+ log?.info({
1641
+ ...telemetry.logFields,
1642
+ ...telemetry.variableLogFields
1643
+ }, "graphql execute start");
1449
1644
  try {
1450
- const result = await run();
1645
+ const result = await executeFn(options);
1451
1646
  if (result && "errors" in result && result.errors?.length) {
1452
- for (const error of result.errors) span.recordException(error);
1453
- span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1454
- }
1647
+ log?.error({
1648
+ ...telemetry.logFields,
1649
+ [FIELD_DURATION_MS]: durationMs(start),
1650
+ ...errorsLogField(result.errors)
1651
+ }, "graphql execute completed with errors");
1652
+ recordSpanErrors(span, result.errors);
1653
+ markErrorsReported(result.errors);
1654
+ } else log?.info({
1655
+ ...telemetry.logFields,
1656
+ [FIELD_DURATION_MS]: durationMs(start)
1657
+ }, "graphql execute completed");
1455
1658
  return result;
1456
1659
  } catch (error) {
1457
- if (error instanceof Error) span.recordException(error);
1458
1660
  log?.error({
1459
- ...operationLogFields(operationName, operationType),
1460
- duration_ms: Date.now() - start,
1661
+ ...telemetry.logFields,
1662
+ [FIELD_DURATION_MS]: durationMs(start),
1461
1663
  ...errorLogField(error)
1462
1664
  }, "graphql execute threw");
1463
- span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1665
+ recordSpanError(span, error);
1666
+ markErrorsReported([error]);
1464
1667
  throw error;
1668
+ }
1669
+ };
1670
+ if (!tracer) return run();
1671
+ return tracer.startActiveSpan(SPAN_EXECUTE, { attributes: telemetry.attributes() }, async (span) => {
1672
+ if (log) {
1673
+ const correlation = traceCorrelationFields(rumbleInput, span);
1674
+ if (Object.keys(correlation).length > 0) log = log.child(correlation);
1675
+ }
1676
+ try {
1677
+ return await run(span);
1465
1678
  } finally {
1466
1679
  span.end();
1467
1680
  }
1468
1681
  });
1469
- try {
1470
- return await run();
1471
- } catch (error) {
1472
- log?.error({
1473
- ...operationLogFields(operationName, operationType),
1474
- duration_ms: Date.now() - start,
1475
- ...errorLogField(error)
1476
- }, "graphql execute threw");
1477
- throw error;
1478
- }
1479
1682
  };
1480
1683
  }
1481
- function buildTracedSubscribe(subscribeFn, rumbleInput) {
1684
+ function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
1685
+ if (!telemetryEnabled(rumbleInput)) return subscribeFn;
1482
1686
  return async (options) => {
1483
- let log = rumbleInput.logger?.enabled ? rumbleInput.logger.logger : void 0;
1484
- const operationName = options.operationName ?? "anonymous";
1485
- const operationType = getOperationType(options);
1486
- const start = Date.now();
1487
- const doSubscribe = async () => {
1488
- log?.info(operationLogFields(operationName, operationType), "graphql subscribe start");
1489
- const result = await subscribeFn(options);
1490
- if (!isAsyncIterable(result)) {
1491
- const execResult = result;
1492
- if (execResult.errors?.length) log?.error({
1493
- ...operationLogFields(operationName, operationType),
1494
- duration_ms: Date.now() - start,
1495
- ...errorsLogField(execResult.errors)
1496
- }, "graphql subscribe completed with errors");
1497
- return execResult;
1498
- }
1687
+ const tracer = telemetryTracer(rumbleInput);
1688
+ let log = telemetryLogger(rumbleInput);
1689
+ const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
1690
+ const start = startTimer();
1691
+ const attributes = tracer ? telemetry.attributes() : {};
1692
+ const doSubscribe = async (span) => {
1499
1693
  log?.info({
1500
- ...operationLogFields(operationName, operationType),
1501
- duration_ms: Date.now() - start
1502
- }, "graphql subscription established");
1503
- if (log) return wrapSubscriptionIterator(result, log, operationName, operationType);
1504
- return result;
1505
- };
1506
- if (rumbleInput.otel?.enabled) return rumbleInput.otel.tracer.startActiveSpan(SUBSCRIBE_SPAN_NAME, { attributes: {
1507
- [_pothos_tracing_opentelemetry.AttributeNames.OPERATION_NAME]: operationName,
1508
- ...operationType ? { [_pothos_tracing_opentelemetry.AttributeNames.OPERATION_TYPE]: operationType } : {},
1509
- "graphql.document": (0, graphql.print)(options.document)
1510
- } }, async (span) => {
1511
- if (log && rumbleInput.logger?.injectTraceId !== false) log = log.child(traceIdLogFields(span));
1694
+ ...telemetry.logFields,
1695
+ ...telemetry.variableLogFields
1696
+ }, "graphql subscribe start");
1512
1697
  try {
1513
- const result = await doSubscribe();
1698
+ const result = await subscribeFn(options);
1514
1699
  if (!isAsyncIterable(result)) {
1515
1700
  const execResult = result;
1516
1701
  if (execResult.errors?.length) {
1517
- for (const error of execResult.errors) span.recordException(error);
1518
- span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1702
+ log?.error({
1703
+ ...telemetry.logFields,
1704
+ [FIELD_DURATION_MS]: durationMs(start),
1705
+ ...errorsLogField(execResult.errors)
1706
+ }, "graphql subscribe completed with errors");
1707
+ recordSpanErrors(span, execResult.errors);
1708
+ markErrorsReported(execResult.errors);
1519
1709
  }
1710
+ return execResult;
1520
1711
  }
1521
- return result;
1712
+ log?.info({
1713
+ ...telemetry.logFields,
1714
+ [FIELD_DURATION_MS]: durationMs(start)
1715
+ }, "graphql subscription established");
1716
+ return wrapSubscriptionIterator(result, {
1717
+ log,
1718
+ tracer,
1719
+ setupSpanContext: span?.spanContext(),
1720
+ logFields: telemetry.logFields,
1721
+ attributes
1722
+ });
1522
1723
  } catch (error) {
1523
- if (error instanceof Error) span.recordException(error);
1524
1724
  log?.error({
1525
- ...operationLogFields(operationName, operationType),
1526
- duration_ms: Date.now() - start,
1725
+ ...telemetry.logFields,
1726
+ [FIELD_DURATION_MS]: durationMs(start),
1527
1727
  ...errorLogField(error)
1528
1728
  }, "graphql subscribe threw");
1529
- span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1729
+ recordSpanError(span, error);
1730
+ markErrorsReported([error]);
1530
1731
  throw error;
1732
+ }
1733
+ };
1734
+ if (!tracer) return doSubscribe();
1735
+ return tracer.startActiveSpan(SPAN_SUBSCRIBE, { attributes }, async (span) => {
1736
+ if (log) {
1737
+ const correlation = traceCorrelationFields(rumbleInput, span);
1738
+ if (Object.keys(correlation).length > 0) log = log.child(correlation);
1739
+ }
1740
+ try {
1741
+ return await doSubscribe(span);
1531
1742
  } finally {
1532
1743
  span.end();
1533
1744
  }
1534
1745
  });
1535
- try {
1536
- return await doSubscribe();
1537
- } catch (error) {
1538
- log?.error({
1539
- ...operationLogFields(operationName, operationType),
1540
- duration_ms: Date.now() - start,
1541
- ...errorLogField(error)
1542
- }, "graphql subscribe threw");
1543
- throw error;
1544
- }
1545
1746
  };
1546
1747
  }
1547
1748
  //#endregion
@@ -1690,17 +1891,30 @@ const isProbablyAConfigObject = (t) => {
1690
1891
  ].find((e) => e === k))) return true;
1691
1892
  return false;
1692
1893
  };
1693
- const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1894
+ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1895
+ const telemetryConfig = {
1896
+ otel,
1897
+ logger: loggerConfig
1898
+ };
1899
+ /**
1900
+ * Schema construction warnings go through the configured logger when there is
1901
+ * one, so they end up in the same structured stream as everything else, and
1902
+ * fall back to the console otherwise.
1903
+ */
1904
+ const warn = (msg, fields = {}) => {
1905
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1906
+ if (log) log.warn({
1907
+ ...fields,
1908
+ ...traceCorrelationFields(telemetryConfig)
1909
+ }, msg);
1910
+ else console.warn(msg);
1911
+ };
1694
1912
  return ({ table, refName, readAction = "read", adjust }) => {
1695
1913
  const tableSchema = tableHelper({
1696
1914
  db,
1697
1915
  table
1698
1916
  });
1699
- if (Object.keys(tableSchema.primaryKey).length === 0) {
1700
- const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1701
- const msg = `Could not find primary key for ${String(table)}. Cannot register subscriptions!`;
1702
- log ? log.warn({ "rumble.table": String(table) }, msg) : console.warn(msg);
1703
- }
1917
+ if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
1704
1918
  const primaryKey = Object.values(tableSchema.primaryKey)[0];
1705
1919
  const { registerOnInstance } = makePubSubInstance({ table });
1706
1920
  return schemaBuilder.drizzleObject(table, {
@@ -1709,9 +1923,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
1709
1923
  if (!primaryKey) return;
1710
1924
  const primaryKeyValue = element[primaryKey.name];
1711
1925
  if (!primaryKeyValue) {
1712
- const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1713
- const msg = `Could not find primary key value for element on ${String(table)}. Cannot register subscription!`;
1714
- log ? log.warn({ "rumble.table": String(table) }, msg) : console.warn(msg);
1926
+ warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
1715
1927
  return;
1716
1928
  }
1717
1929
  registerOnInstance({
@@ -1743,7 +1955,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
1743
1955
  } })) ?? {};
1744
1956
  const fields = Object.entries(columns).reduce((acc, [key, column]) => {
1745
1957
  if (key.startsWith("__")) {
1746
- 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.`);
1958
+ 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 });
1747
1959
  return acc;
1748
1960
  }
1749
1961
  if (userAdjustments[key]) {
@@ -1946,8 +2158,12 @@ const createPubSubInstance = ({ subscriptions }) => {
1946
2158
  };
1947
2159
  //#endregion
1948
2160
  //#region lib/query.ts
1949
- const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
2161
+ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1950
2162
  const registeredQueryFieldNames = /* @__PURE__ */ new Set();
2163
+ const telemetryConfig = {
2164
+ otel,
2165
+ logger: loggerConfig
2166
+ };
1951
2167
  return ({ table, readAction = "read", listAction = "read", findFirst, findMany }) => {
1952
2168
  const WhereArg = whereArgImplementer({ table });
1953
2169
  const OrderArg = orderArgImplementer({ table });
@@ -2019,7 +2235,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfi
2019
2235
  else {
2020
2236
  const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
2021
2237
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
2022
- log ? log.info({}, msg) : console.info(msg);
2238
+ log ? log.info(traceCorrelationFields(telemetryConfig), msg) : console.info(msg);
2023
2239
  }
2024
2240
  return run(tx);
2025
2241
  });
@@ -2311,6 +2527,7 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
2311
2527
  tracer;
2312
2528
  tracerEnabled;
2313
2529
  logger;
2530
+ telemetryConfig = {};
2314
2531
  filterLoaders = /* @__PURE__ */ new WeakMap();
2315
2532
  getLoader(context, filter) {
2316
2533
  let perContext = this.filterLoaders.get(context);
@@ -2339,6 +2556,10 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
2339
2556
  this.tracer = this.builder.options.otel?.tracer;
2340
2557
  this.tracerEnabled = this.builder.options.otel?.enabled;
2341
2558
  this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
2559
+ this.telemetryConfig = {
2560
+ otel: this.builder.options.otel,
2561
+ logger: this.builder.options.logger
2562
+ };
2342
2563
  return typeConfig;
2343
2564
  }
2344
2565
  wrapResolve(resolver, fieldConfig) {
@@ -2350,13 +2571,16 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
2350
2571
  if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
2351
2572
  const runFilters = async (span) => {
2352
2573
  const allFilters = Array.isArray(filters) ? filters : [filters];
2353
- span?.setAttribute("filters.total", allFilters.length);
2574
+ span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
2354
2575
  const loaders = allFilters.map((filter) => this.getLoader(context, filter));
2355
2576
  let resolved;
2356
- if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(`rumble.filter.resolve`, async (span) => {
2357
- span.setAttribute("graphql.field.name", fieldConfig.name);
2577
+ if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(SPAN_FILTERS_RESOLVE, async (span) => {
2578
+ span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
2358
2579
  try {
2359
2580
  return await resolver(parent, args, context, info);
2581
+ } catch (error) {
2582
+ recordSpanError(span, error);
2583
+ throw error;
2360
2584
  } finally {
2361
2585
  span.end();
2362
2586
  }
@@ -2365,24 +2589,30 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
2365
2589
  const entities = Array.isArray(resolved) ? resolved : [resolved];
2366
2590
  const perFilterResults = await Promise.all(loaders.map((loader) => Promise.all(entities.map((entity) => loader.load(entity)))));
2367
2591
  const allowed = entities.filter((_, index) => perFilterResults.some((results) => results[index] != null));
2368
- span?.setAttribute("filters.allowed", allowed.length);
2592
+ span?.setAttribute(ATTR_FILTERS_ALLOWED, allowed.length);
2369
2593
  this.logger?.debug({
2370
- "graphql.field.name": fieldConfig.name,
2371
- "filters.total": allFilters.length,
2372
- "filters.allowed": allowed.length
2594
+ [ATTR_FIELD_NAME]: fieldConfig.name,
2595
+ [ATTR_FILTERS_TOTAL]: allFilters.length,
2596
+ [ATTR_FILTERS_ALLOWED]: allowed.length,
2597
+ ...traceCorrelationFields(this.telemetryConfig)
2373
2598
  }, "runtime filters applied");
2374
2599
  if (Array.isArray(resolved)) return allowed;
2375
2600
  return allowed[0] ?? null;
2376
2601
  };
2377
- if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`rumble.filter.apply`, async (span) => {
2378
- span.setAttribute("graphql.field.name", fieldConfig.name);
2602
+ const logFilterError = (error) => {
2603
+ this.logger?.error({
2604
+ [ATTR_FIELD_NAME]: fieldConfig.name,
2605
+ ...traceCorrelationFields(this.telemetryConfig),
2606
+ ...errorLogField(error)
2607
+ }, "runtime filter threw");
2608
+ };
2609
+ if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(SPAN_FILTERS_APPLY, async (span) => {
2610
+ span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
2379
2611
  try {
2380
2612
  return await runFilters(span);
2381
2613
  } catch (error) {
2382
- this.logger?.error({
2383
- "graphql.field.name": fieldConfig.name,
2384
- ...errorLogField(error)
2385
- }, "runtime filter threw");
2614
+ logFilterError(error);
2615
+ recordSpanError(span, error);
2386
2616
  throw error;
2387
2617
  } finally {
2388
2618
  span.end();
@@ -2391,10 +2621,7 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
2391
2621
  else try {
2392
2622
  return await runFilters();
2393
2623
  } catch (error) {
2394
- this.logger?.error({
2395
- "graphql.field.name": fieldConfig.name,
2396
- ...errorLogField(error)
2397
- }, "runtime filter threw");
2624
+ logFilterError(error);
2398
2625
  throw error;
2399
2626
  }
2400
2627
  };
@@ -2442,18 +2669,22 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2442
2669
  let r = createSpan ? createSpan(resolver, options) : resolver;
2443
2670
  if (logger?.enabled) {
2444
2671
  const log = logger.logger;
2672
+ const telemetryConfig = {
2673
+ otel,
2674
+ logger
2675
+ };
2445
2676
  r = (0, _pothos_plugin_tracing.wrapResolver)(r, (error, duration) => {
2677
+ const fields = {
2678
+ [ATTR_FIELD_NAME]: config.name,
2679
+ [ATTR_PARENT_TYPE]: config.parentType,
2680
+ [FIELD_DURATION_MS]: duration,
2681
+ ...traceCorrelationFields(telemetryConfig)
2682
+ };
2446
2683
  if (error) log.error({
2447
- "graphql.field.name": config.name,
2448
- "graphql.parent.type": config.parentType,
2449
- duration_ms: duration,
2684
+ ...fields,
2450
2685
  ...errorLogField(error)
2451
2686
  }, "resolver failed");
2452
- else log.debug({
2453
- "graphql.field.name": config.name,
2454
- "graphql.parent.type": config.parentType,
2455
- duration_ms: duration
2456
- }, "resolver completed");
2687
+ else log.debug(fields, "resolver completed");
2457
2688
  });
2458
2689
  }
2459
2690
  return r;
@@ -2475,6 +2706,27 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2475
2706
  };
2476
2707
  //#endregion
2477
2708
  //#region lib/rumble.ts
2709
+ /**
2710
+ * Mirrors sofa's own default error handler: it derives the HTTP status from
2711
+ * `extensions.http` and returns the errors as JSON. rumble only adds telemetry
2712
+ * around error handling, so a request that fails must still answer with the
2713
+ * status and body a plain sofa setup would have produced.
2714
+ */
2715
+ function sofaErrorResponse(errors) {
2716
+ let status;
2717
+ const headers = { "Content-Type": "application/json; charset=utf-8" };
2718
+ for (const error of errors) {
2719
+ const http = error?.extensions?.http;
2720
+ if (!http) continue;
2721
+ if (http.status && (!status || http.status > status)) status = http.status;
2722
+ if (http.headers) Object.assign(headers, http.headers);
2723
+ delete error.extensions.http;
2724
+ }
2725
+ return Response.json({ errors }, {
2726
+ status: status ?? 500,
2727
+ headers
2728
+ });
2729
+ }
2478
2730
  const rumble = (rumbleInput) => {
2479
2731
  if (!rumbleInput.db._.relations) throw new RumbleError(`
2480
2732
  rumble could not find any relations in the provided drizzle instance.
@@ -2580,6 +2832,33 @@ export const r = rumble({
2580
2832
  types: filteredTypes
2581
2833
  });
2582
2834
  });
2835
+ /**
2836
+ * Adapts the configured rumble logger to the logger interface yoga expects,
2837
+ * so yoga's own diagnostics end up in the same structured stream (with the
2838
+ * same `rumble.transport` and trace correlation fields) as rumble's entries.
2839
+ */
2840
+ const yogaLogging = () => {
2841
+ const log = telemetryLogger(rumbleInput);
2842
+ if (!log) return void 0;
2843
+ const emit = (level) => (...args) => {
2844
+ const messages = args.filter((arg) => typeof arg === "string");
2845
+ const rest = args.filter((arg) => typeof arg !== "string");
2846
+ const errors = rest.filter((entry) => entry instanceof Error);
2847
+ const details = rest.filter((entry) => !(entry instanceof Error));
2848
+ log[level]({
2849
+ [ATTR_TRANSPORT]: "graphql",
2850
+ ...traceCorrelationFields(rumbleInput),
2851
+ ...errors.length > 0 ? errorsLogField(errors) : {},
2852
+ ...details.length > 0 ? { details } : {}
2853
+ }, messages.join(" ") || "yoga diagnostic");
2854
+ };
2855
+ return {
2856
+ debug: emit("debug"),
2857
+ info: emit("info"),
2858
+ warn: emit("warn"),
2859
+ error: emit("error")
2860
+ };
2861
+ };
2583
2862
  const createYoga = (args) => {
2584
2863
  const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
2585
2864
  return (0, graphql_yoga.createYoga)({
@@ -2587,38 +2866,41 @@ export const r = rumble({
2587
2866
  graphiql: enableApiDocs,
2588
2867
  schema: builtSchema(),
2589
2868
  context,
2869
+ logging: args?.logging ?? yogaLogging() ?? true,
2590
2870
  plugins: [
2591
2871
  ...args?.plugins ?? [],
2592
2872
  ...enableApiDocs ? [] : [(0, _graphql_yoga_plugin_disable_introspection.useDisableIntrospection)(), (0, _escape_tech_graphql_armor.EnvelopArmorPlugin)(args?.armorConfig)],
2593
- rumbleInput.otel?.enabled || rumbleInput.logger?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
2594
- setExecuteFn(buildTracedExecute(executeFn, rumbleInput));
2595
- } } : false
2873
+ telemetryEnabled(rumbleInput) ? {
2874
+ onExecute: ({ setExecuteFn, executeFn }) => {
2875
+ setExecuteFn(buildTracedExecute(executeFn, rumbleInput, "graphql"));
2876
+ },
2877
+ onSubscribe: ({ setSubscribeFn, subscribeFn }) => {
2878
+ setSubscribeFn(buildTracedSubscribe(subscribeFn, rumbleInput, "graphql"));
2879
+ }
2880
+ } : false
2596
2881
  ].filter(Boolean)
2597
2882
  });
2598
2883
  };
2599
2884
  const createSofa = async (args) => {
2600
2885
  const { useSofa: useSofaFn } = await import("@m1212e/sofa-api");
2601
2886
  if (args.openAPI) (0, es_toolkit.merge)(args.openAPI, sofaOpenAPIWebhookDocs);
2602
- if (args.errorHandler) {
2603
- const originalHandler = args.errorHandler;
2604
- args.errorHandler = (errors) => {
2605
- const span = _opentelemetry_api.trace.getActiveSpan();
2606
- for (const error of errors) span?.recordException(error);
2607
- span?.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2608
- return originalHandler(errors);
2609
- };
2610
- }
2887
+ const userErrorHandler = args.errorHandler;
2611
2888
  return useSofaFn({
2612
2889
  ...args,
2613
2890
  schema: builtSchema(),
2614
2891
  context,
2615
- execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput),
2616
- subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput),
2892
+ execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput, "rest"),
2893
+ subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput, "rest"),
2617
2894
  errorHandler(errors) {
2618
- const span = _opentelemetry_api.trace.getActiveSpan();
2619
- for (const error of errors) span?.recordException(error);
2620
- span?.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2621
- return new Response(errors[0].message, { status: 500 });
2895
+ if (!errorsAlreadyReported(errors)) {
2896
+ recordSpanErrors(_opentelemetry_api.trace.getActiveSpan(), errors);
2897
+ telemetryLogger(rumbleInput)?.error({
2898
+ [ATTR_TRANSPORT]: "rest",
2899
+ ...traceCorrelationFields(rumbleInput),
2900
+ ...errorsLogField(errors)
2901
+ }, "rest request failed");
2902
+ }
2903
+ return userErrorHandler?.(errors) ?? sofaErrorResponse(errors);
2622
2904
  }
2623
2905
  });
2624
2906
  };
@@ -2627,8 +2909,8 @@ export const r = rumble({
2627
2909
  ...args,
2628
2910
  schema: builtSchema(),
2629
2911
  context,
2630
- execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput),
2631
- subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput)
2912
+ execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput, "ws"),
2913
+ subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput, "ws")
2632
2914
  }, ...rest);
2633
2915
  };
2634
2916
  return {