@m1212e/rumble 0.23.15 → 0.23.18

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.15";
185
+ var version = "0.23.18";
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,164 @@ 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
+ function safeJsonStringify(value) {
567
+ try {
568
+ return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner) ?? "null";
569
+ } catch {
570
+ return String(value);
571
+ }
572
+ }
573
+ function isHomogeneousPrimitiveArray(value) {
574
+ let seen;
575
+ for (const entry of value) {
576
+ if (entry === null || entry === void 0) continue;
577
+ const type = typeof entry;
578
+ if (type !== "string" && type !== "number" && type !== "boolean") return false;
579
+ if (seen && seen !== type) return false;
580
+ seen = type;
581
+ }
582
+ return true;
583
+ }
584
+ /**
585
+ * Coerces an arbitrary value into something the OpenTelemetry attribute type
586
+ * allows: primitives pass through, homogeneous primitive arrays stay arrays
587
+ * (mixed ones are invalid per spec) and everything else becomes JSON.
588
+ *
589
+ * Values are intentionally not truncated here — length limits belong to the SDK
590
+ * (`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`) so they stay configurable per deployment.
591
+ */
592
+ function attributeValue(value) {
593
+ switch (typeof value) {
594
+ case "string": return value;
595
+ case "number": return Number.isFinite(value) ? value : String(value);
596
+ case "boolean": return value;
597
+ case "bigint": return value.toString();
598
+ case "undefined": return "null";
599
+ default: break;
600
+ }
601
+ if (value === null) return "null";
602
+ if (Array.isArray(value) && isHomogeneousPrimitiveArray(value)) return value;
603
+ return safeJsonStringify(value);
604
+ }
605
+ function resolveVariables(variables, include) {
606
+ if (include === false || !variables) return void 0;
607
+ const resolved = typeof include === "function" ? include({ ...variables }) : variables;
608
+ if (!resolved || Object.keys(resolved).length === 0) return void 0;
609
+ return resolved;
610
+ }
611
+ /** Variables flattened into `graphql.variables.<name>` span attributes. */
612
+ function variableAttributes(variables, include) {
613
+ const resolved = resolveVariables(variables, include);
614
+ if (!resolved) return {};
615
+ const attributes = {};
616
+ for (const [key, value] of Object.entries(resolved)) attributes[`${ATTR_VARIABLES_PREFIX}${key}`] = attributeValue(value);
617
+ return attributes;
618
+ }
619
+ /** Variables as a single nested `graphql.variables` log field. */
620
+ function variableLogField(variables, include) {
621
+ const resolved = resolveVariables(variables, include);
622
+ return resolved ? { [FIELD_VARIABLES]: resolved } : {};
623
+ }
624
+ /**
625
+ * Monotonic clock reading. `Date.now()` can jump backwards when the wall clock is
626
+ * adjusted (NTP), which would produce negative or inflated durations.
627
+ */
628
+ function startTimer() {
629
+ return performance.now();
630
+ }
631
+ function durationMs(start) {
632
+ return Math.round((performance.now() - start) * 100) / 100;
633
+ }
634
+ //#endregion
444
635
  //#region lib/abilityBuilder.ts
445
636
  function isDynamicQueryFilter(filter) {
446
637
  return typeof filter === "function" && filter.constructor.name !== "AsyncFunction";
@@ -448,17 +639,22 @@ function isDynamicQueryFilter(filter) {
448
639
  function isStaticQueryFilter(filter) {
449
640
  return typeof filter !== "function";
450
641
  }
451
- const makeNothingRegisteredWarner = (logger) => debounce((model, action) => {
642
+ const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
452
643
  const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
453
644
  if (logger) logger.warn({
454
- "rumble.table": model,
455
- "rumble.action": action
645
+ [ATTR_TABLE]: model,
646
+ [ATTR_ACTION]: action,
647
+ ...traceCorrelationFields(config)
456
648
  }, msg);
457
649
  else console.warn(msg);
458
650
  }, 1e3);
459
651
  const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
460
652
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
461
- const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(log);
653
+ const telemetryConfig = {
654
+ otel,
655
+ logger: loggerConfig
656
+ };
657
+ const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
462
658
  const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
463
659
  db,
464
660
  table: name
@@ -718,14 +914,14 @@ by: (explicitFilter) => {
718
914
  }
719
915
  return { withContext: (userContext) => {
720
916
  return { filter: (action) => {
721
- const assembleAbilities = (span) => {
917
+ const assembleAbilities = (attributes) => {
722
918
  const filters = queryFilters.get(action);
723
919
  if (filters === "unrestricted") {
724
- span?.setAttribute("abilities.status", "unrestricted");
920
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
725
921
  return transformToResponse();
726
922
  }
727
923
  if (!filters) {
728
- span?.setAttribute("abilities.status", "blocked_everything");
924
+ attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
729
925
  nothingRegisteredWarningLogger(String(tableName), action);
730
926
  return transformToResponse(blockEverythingFilter);
731
927
  }
@@ -734,58 +930,60 @@ by: (explicitFilter) => {
734
930
  for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
735
931
  const func = dynamicQueryFilters[action][i];
736
932
  const result = func(userContext);
737
- if (result === "allow") return transformToResponse();
933
+ if (result === "allow") {
934
+ attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
935
+ return transformToResponse();
936
+ }
738
937
  if (result === void 0) continue;
739
938
  dynamicResults[filtersReturned++] = result;
740
939
  }
741
940
  dynamicResults.length = filtersReturned;
742
- span?.setAttribute("abilities.dynamic", dynamicResults.length);
743
- span?.setAttribute("abilities.static", simpleQueryFilters[action].length);
941
+ attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
942
+ attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
744
943
  const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
745
- span?.setAttribute("abilities.total", allQueryFilters.length);
944
+ attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
746
945
  if (allQueryFilters.length === 0) {
747
- span?.setAttribute("abilities.status", "blocked_everything");
946
+ attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
748
947
  return transformToResponse(blockEverythingFilter);
749
948
  }
750
949
  const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
751
950
  return mergeFilters(a, b, "OR");
752
951
  });
753
- span?.setAttribute("abilities.status", "applied");
952
+ attributes[ATTR_ABILITIES_STATUS] = "applied";
754
953
  return transformToResponse(mergedFilters);
755
954
  };
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));
955
+ const run = (span) => {
956
+ const attributes = {
957
+ [ATTR_TABLE]: String(tableName),
958
+ [ATTR_ACTION]: action
959
+ };
759
960
  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");
961
+ const result = assembleAbilities(attributes);
962
+ log?.debug({
963
+ ...attributes,
964
+ ...traceCorrelationFields(telemetryConfig, span)
965
+ }, "abilities prepared");
773
966
  return result;
967
+ } catch (error) {
968
+ recordSpanError(span, error);
969
+ log?.error({
970
+ ...attributes,
971
+ ...traceCorrelationFields(telemetryConfig, span),
972
+ ...errorLogField(error)
973
+ }, "abilities failed");
974
+ throw error;
975
+ } finally {
976
+ span?.setAttributes(attributes);
977
+ }
978
+ };
979
+ if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, (span) => {
980
+ try {
981
+ return run(span);
774
982
  } finally {
775
983
  span.end();
776
984
  }
777
985
  });
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
- }
986
+ return run();
789
987
  } };
790
988
  } };
791
989
  };
@@ -1312,207 +1510,188 @@ const sofaOpenAPIWebhookDocs = {
1312
1510
  } }
1313
1511
  };
1314
1512
  //#endregion
1315
- //#region lib/helpers/errorLogging.ts
1316
- function hasMessage(error) {
1317
- return typeof error === "object" && error !== null && "message" in error;
1513
+ //#region lib/helpers/tracing.ts
1514
+ function isAsyncIterable(value) {
1515
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
1318
1516
  }
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
- }
1517
+ function buildOperationTelemetry(config, transport, options) {
1518
+ const operationAst = getOperationAST(options.document, options.operationName);
1519
+ const operationName = options.operationName ?? operationAst?.name?.value ?? "anonymous";
1520
+ const operationType = operationAst?.operation;
1521
+ const logFields = {
1522
+ [ATTR_OPERATION_NAME]: operationName,
1523
+ ...operationType ? { [ATTR_OPERATION_TYPE]: operationType } : {},
1524
+ [ATTR_TRANSPORT]: transport
1525
+ };
1330
1526
  return {
1331
- "exception.type": "UnknownError",
1332
- "exception.message": String(error)
1527
+ operationName,
1528
+ operationType,
1529
+ logFields,
1530
+ variableLogFields: variableLogField(options.variableValues, config.logger?.includeVariables),
1531
+ attributes: () => ({
1532
+ ...logFields,
1533
+ ...config.otel?.includeDocument === false ? {} : { [ATTR_DOCUMENT]: print(options.document) },
1534
+ ...variableAttributes(options.variableValues, config.otel?.includeVariables)
1535
+ })
1333
1536
  };
1334
1537
  }
1335
1538
  /**
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).
1539
+ * Wraps a subscription's event stream so delivered events are observable no
1540
+ * matter which transport consumes them.
1339
1541
  */
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")
1542
+ async function* wrapSubscriptionIterator(iterator, telemetry) {
1543
+ const { log, tracer, setupSpanContext, logFields, attributes } = telemetry;
1544
+ const recordEventErrors = (eventCount, errors) => {
1545
+ if (!tracer) return;
1546
+ const span = tracer.startSpan(SPAN_SUBSCRIBE_EVENT, {
1547
+ root: true,
1548
+ links: setupSpanContext ? [{ context: setupSpanContext }] : void 0,
1549
+ attributes: {
1550
+ ...attributes,
1551
+ [ATTR_SUBSCRIPTION_EVENT_INDEX]: eventCount
1552
+ }
1553
+ });
1554
+ recordSpanErrors(span, errors);
1555
+ span.end();
1364
1556
  };
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
1557
  let eventCount = 0;
1371
1558
  try {
1372
1559
  for await (const event of iterator) {
1373
1560
  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");
1561
+ if (event.errors?.length) {
1562
+ log?.error({
1563
+ ...logFields,
1564
+ [FIELD_EVENT_COUNT]: eventCount,
1565
+ ...errorsLogField(event.errors)
1566
+ }, "graphql subscription event error");
1567
+ recordEventErrors(eventCount, event.errors);
1568
+ }
1379
1569
  yield event;
1380
1570
  }
1381
- log.info({
1382
- ...operationLogFields(operationName, operationType),
1383
- event_count: eventCount
1571
+ log?.info({
1572
+ ...logFields,
1573
+ [FIELD_EVENT_COUNT]: eventCount
1384
1574
  }, "graphql subscription completed");
1385
1575
  } catch (error) {
1386
- log.error({
1387
- ...operationLogFields(operationName, operationType),
1388
- event_count: eventCount,
1576
+ log?.error({
1577
+ ...logFields,
1578
+ [FIELD_EVENT_COUNT]: eventCount,
1389
1579
  ...errorLogField(error)
1390
1580
  }, "graphql subscription threw");
1581
+ recordEventErrors(eventCount, [error]);
1391
1582
  throw error;
1392
1583
  }
1393
1584
  }
1394
- function buildTracedExecute(executeFn, rumbleInput) {
1585
+ function buildTracedExecute(executeFn, rumbleInput, transport) {
1586
+ if (!telemetryEnabled(rumbleInput)) return executeFn;
1395
1587
  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));
1588
+ const tracer = telemetryTracer(rumbleInput);
1589
+ let log = telemetryLogger(rumbleInput);
1590
+ const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
1591
+ const start = startTimer();
1592
+ const run = async (span) => {
1593
+ log?.info({
1594
+ ...telemetry.logFields,
1595
+ ...telemetry.variableLogFields
1596
+ }, "graphql execute start");
1420
1597
  try {
1421
- const result = await run();
1598
+ const result = await executeFn(options);
1422
1599
  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
- }
1600
+ log?.error({
1601
+ ...telemetry.logFields,
1602
+ [FIELD_DURATION_MS]: durationMs(start),
1603
+ ...errorsLogField(result.errors)
1604
+ }, "graphql execute completed with errors");
1605
+ recordSpanErrors(span, result.errors);
1606
+ } else log?.info({
1607
+ ...telemetry.logFields,
1608
+ [FIELD_DURATION_MS]: durationMs(start)
1609
+ }, "graphql execute completed");
1426
1610
  return result;
1427
1611
  } catch (error) {
1428
- if (error instanceof Error) span.recordException(error);
1429
1612
  log?.error({
1430
- ...operationLogFields(operationName, operationType),
1431
- duration_ms: Date.now() - start,
1613
+ ...telemetry.logFields,
1614
+ [FIELD_DURATION_MS]: durationMs(start),
1432
1615
  ...errorLogField(error)
1433
1616
  }, "graphql execute threw");
1434
- span.setStatus({ code: SpanStatusCode.ERROR });
1617
+ recordSpanError(span, error);
1435
1618
  throw error;
1619
+ }
1620
+ };
1621
+ if (!tracer) return run();
1622
+ return tracer.startActiveSpan(SPAN_EXECUTE, { attributes: telemetry.attributes() }, async (span) => {
1623
+ if (log) {
1624
+ const correlation = traceCorrelationFields(rumbleInput, span);
1625
+ if (Object.keys(correlation).length > 0) log = log.child(correlation);
1626
+ }
1627
+ try {
1628
+ return await run(span);
1436
1629
  } finally {
1437
1630
  span.end();
1438
1631
  }
1439
1632
  });
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
1633
  };
1451
1634
  }
1452
- function buildTracedSubscribe(subscribeFn, rumbleInput) {
1635
+ function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
1636
+ if (!telemetryEnabled(rumbleInput)) return subscribeFn;
1453
1637
  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
- }
1638
+ const tracer = telemetryTracer(rumbleInput);
1639
+ let log = telemetryLogger(rumbleInput);
1640
+ const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
1641
+ const start = startTimer();
1642
+ const attributes = tracer ? telemetry.attributes() : {};
1643
+ const doSubscribe = async (span) => {
1470
1644
  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));
1645
+ ...telemetry.logFields,
1646
+ ...telemetry.variableLogFields
1647
+ }, "graphql subscribe start");
1483
1648
  try {
1484
- const result = await doSubscribe();
1649
+ const result = await subscribeFn(options);
1485
1650
  if (!isAsyncIterable(result)) {
1486
1651
  const execResult = result;
1487
1652
  if (execResult.errors?.length) {
1488
- for (const error of execResult.errors) span.recordException(error);
1489
- span.setStatus({ code: SpanStatusCode.ERROR });
1653
+ log?.error({
1654
+ ...telemetry.logFields,
1655
+ [FIELD_DURATION_MS]: durationMs(start),
1656
+ ...errorsLogField(execResult.errors)
1657
+ }, "graphql subscribe completed with errors");
1658
+ recordSpanErrors(span, execResult.errors);
1490
1659
  }
1660
+ return execResult;
1491
1661
  }
1492
- return result;
1662
+ log?.info({
1663
+ ...telemetry.logFields,
1664
+ [FIELD_DURATION_MS]: durationMs(start)
1665
+ }, "graphql subscription established");
1666
+ return wrapSubscriptionIterator(result, {
1667
+ log,
1668
+ tracer,
1669
+ setupSpanContext: span?.spanContext(),
1670
+ logFields: telemetry.logFields,
1671
+ attributes
1672
+ });
1493
1673
  } catch (error) {
1494
- if (error instanceof Error) span.recordException(error);
1495
1674
  log?.error({
1496
- ...operationLogFields(operationName, operationType),
1497
- duration_ms: Date.now() - start,
1675
+ ...telemetry.logFields,
1676
+ [FIELD_DURATION_MS]: durationMs(start),
1498
1677
  ...errorLogField(error)
1499
1678
  }, "graphql subscribe threw");
1500
- span.setStatus({ code: SpanStatusCode.ERROR });
1679
+ recordSpanError(span, error);
1501
1680
  throw error;
1681
+ }
1682
+ };
1683
+ if (!tracer) return doSubscribe();
1684
+ return tracer.startActiveSpan(SPAN_SUBSCRIBE, { attributes }, async (span) => {
1685
+ if (log) {
1686
+ const correlation = traceCorrelationFields(rumbleInput, span);
1687
+ if (Object.keys(correlation).length > 0) log = log.child(correlation);
1688
+ }
1689
+ try {
1690
+ return await doSubscribe(span);
1502
1691
  } finally {
1503
1692
  span.end();
1504
1693
  }
1505
1694
  });
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
1695
  };
1517
1696
  }
1518
1697
  //#endregion
@@ -1661,17 +1840,30 @@ const isProbablyAConfigObject = (t) => {
1661
1840
  ].find((e) => e === k))) return true;
1662
1841
  return false;
1663
1842
  };
1664
- const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1843
+ const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
1844
+ const telemetryConfig = {
1845
+ otel,
1846
+ logger: loggerConfig
1847
+ };
1848
+ /**
1849
+ * Schema construction warnings go through the configured logger when there is
1850
+ * one, so they end up in the same structured stream as everything else, and
1851
+ * fall back to the console otherwise.
1852
+ */
1853
+ const warn = (msg, fields = {}) => {
1854
+ const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1855
+ if (log) log.warn({
1856
+ ...fields,
1857
+ ...traceCorrelationFields(telemetryConfig)
1858
+ }, msg);
1859
+ else console.warn(msg);
1860
+ };
1665
1861
  return ({ table, refName, readAction = "read", adjust }) => {
1666
1862
  const tableSchema = tableHelper({
1667
1863
  db,
1668
1864
  table
1669
1865
  });
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
- }
1866
+ if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
1675
1867
  const primaryKey = Object.values(tableSchema.primaryKey)[0];
1676
1868
  const { registerOnInstance } = makePubSubInstance({ table });
1677
1869
  return schemaBuilder.drizzleObject(table, {
@@ -1680,9 +1872,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
1680
1872
  if (!primaryKey) return;
1681
1873
  const primaryKeyValue = element[primaryKey.name];
1682
1874
  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);
1875
+ warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
1686
1876
  return;
1687
1877
  }
1688
1878
  registerOnInstance({
@@ -1714,7 +1904,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
1714
1904
  } })) ?? {};
1715
1905
  const fields = Object.entries(columns).reduce((acc, [key, column]) => {
1716
1906
  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.`);
1907
+ 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
1908
  return acc;
1719
1909
  }
1720
1910
  if (userAdjustments[key]) {
@@ -1917,8 +2107,12 @@ const createPubSubInstance = ({ subscriptions }) => {
1917
2107
  };
1918
2108
  //#endregion
1919
2109
  //#region lib/query.ts
1920
- const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
2110
+ const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
1921
2111
  const registeredQueryFieldNames = /* @__PURE__ */ new Set();
2112
+ const telemetryConfig = {
2113
+ otel,
2114
+ logger: loggerConfig
2115
+ };
1922
2116
  return ({ table, readAction = "read", listAction = "read", findFirst, findMany }) => {
1923
2117
  const WhereArg = whereArgImplementer({ table });
1924
2118
  const OrderArg = orderArgImplementer({ table });
@@ -1990,7 +2184,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfi
1990
2184
  else {
1991
2185
  const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
1992
2186
  const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
1993
- log ? log.info({}, msg) : console.info(msg);
2187
+ log ? log.info(traceCorrelationFields(telemetryConfig), msg) : console.info(msg);
1994
2188
  }
1995
2189
  return run(tx);
1996
2190
  });
@@ -2282,6 +2476,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2282
2476
  tracer;
2283
2477
  tracerEnabled;
2284
2478
  logger;
2479
+ telemetryConfig = {};
2285
2480
  filterLoaders = /* @__PURE__ */ new WeakMap();
2286
2481
  getLoader(context, filter) {
2287
2482
  let perContext = this.filterLoaders.get(context);
@@ -2310,6 +2505,10 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2310
2505
  this.tracer = this.builder.options.otel?.tracer;
2311
2506
  this.tracerEnabled = this.builder.options.otel?.enabled;
2312
2507
  this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
2508
+ this.telemetryConfig = {
2509
+ otel: this.builder.options.otel,
2510
+ logger: this.builder.options.logger
2511
+ };
2313
2512
  return typeConfig;
2314
2513
  }
2315
2514
  wrapResolve(resolver, fieldConfig) {
@@ -2321,13 +2520,16 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2321
2520
  if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
2322
2521
  const runFilters = async (span) => {
2323
2522
  const allFilters = Array.isArray(filters) ? filters : [filters];
2324
- span?.setAttribute("filters.total", allFilters.length);
2523
+ span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
2325
2524
  const loaders = allFilters.map((filter) => this.getLoader(context, filter));
2326
2525
  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);
2526
+ if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(SPAN_FILTERS_RESOLVE, async (span) => {
2527
+ span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
2329
2528
  try {
2330
2529
  return await resolver(parent, args, context, info);
2530
+ } catch (error) {
2531
+ recordSpanError(span, error);
2532
+ throw error;
2331
2533
  } finally {
2332
2534
  span.end();
2333
2535
  }
@@ -2336,24 +2538,30 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2336
2538
  const entities = Array.isArray(resolved) ? resolved : [resolved];
2337
2539
  const perFilterResults = await Promise.all(loaders.map((loader) => Promise.all(entities.map((entity) => loader.load(entity)))));
2338
2540
  const allowed = entities.filter((_, index) => perFilterResults.some((results) => results[index] != null));
2339
- span?.setAttribute("filters.allowed", allowed.length);
2541
+ span?.setAttribute(ATTR_FILTERS_ALLOWED, allowed.length);
2340
2542
  this.logger?.debug({
2341
- "graphql.field.name": fieldConfig.name,
2342
- "filters.total": allFilters.length,
2343
- "filters.allowed": allowed.length
2543
+ [ATTR_FIELD_NAME]: fieldConfig.name,
2544
+ [ATTR_FILTERS_TOTAL]: allFilters.length,
2545
+ [ATTR_FILTERS_ALLOWED]: allowed.length,
2546
+ ...traceCorrelationFields(this.telemetryConfig)
2344
2547
  }, "runtime filters applied");
2345
2548
  if (Array.isArray(resolved)) return allowed;
2346
2549
  return allowed[0] ?? null;
2347
2550
  };
2348
- if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(`rumble.filter.apply`, async (span) => {
2349
- span.setAttribute("graphql.field.name", fieldConfig.name);
2551
+ const logFilterError = (error) => {
2552
+ this.logger?.error({
2553
+ [ATTR_FIELD_NAME]: fieldConfig.name,
2554
+ ...traceCorrelationFields(this.telemetryConfig),
2555
+ ...errorLogField(error)
2556
+ }, "runtime filter threw");
2557
+ };
2558
+ if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(SPAN_FILTERS_APPLY, async (span) => {
2559
+ span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
2350
2560
  try {
2351
2561
  return await runFilters(span);
2352
2562
  } catch (error) {
2353
- this.logger?.error({
2354
- "graphql.field.name": fieldConfig.name,
2355
- ...errorLogField(error)
2356
- }, "runtime filter threw");
2563
+ logFilterError(error);
2564
+ recordSpanError(span, error);
2357
2565
  throw error;
2358
2566
  } finally {
2359
2567
  span.end();
@@ -2362,10 +2570,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
2362
2570
  else try {
2363
2571
  return await runFilters();
2364
2572
  } catch (error) {
2365
- this.logger?.error({
2366
- "graphql.field.name": fieldConfig.name,
2367
- ...errorLogField(error)
2368
- }, "runtime filter threw");
2573
+ logFilterError(error);
2369
2574
  throw error;
2370
2575
  }
2371
2576
  };
@@ -2413,18 +2618,22 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2413
2618
  let r = createSpan ? createSpan(resolver, options) : resolver;
2414
2619
  if (logger?.enabled) {
2415
2620
  const log = logger.logger;
2621
+ const telemetryConfig = {
2622
+ otel,
2623
+ logger
2624
+ };
2416
2625
  r = wrapResolver(r, (error, duration) => {
2626
+ const fields = {
2627
+ [ATTR_FIELD_NAME]: config.name,
2628
+ [ATTR_PARENT_TYPE]: config.parentType,
2629
+ [FIELD_DURATION_MS]: duration,
2630
+ ...traceCorrelationFields(telemetryConfig)
2631
+ };
2417
2632
  if (error) log.error({
2418
- "graphql.field.name": config.name,
2419
- "graphql.parent.type": config.parentType,
2420
- duration_ms: duration,
2633
+ ...fields,
2421
2634
  ...errorLogField(error)
2422
2635
  }, "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");
2636
+ else log.debug(fields, "resolver completed");
2428
2637
  });
2429
2638
  }
2430
2639
  return r;
@@ -2446,6 +2655,27 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
2446
2655
  };
2447
2656
  //#endregion
2448
2657
  //#region lib/rumble.ts
2658
+ /**
2659
+ * Mirrors sofa's own default error handler: it derives the HTTP status from
2660
+ * `extensions.http` and returns the errors as JSON. rumble only adds telemetry
2661
+ * around error handling, so a request that fails must still answer with the
2662
+ * status and body a plain sofa setup would have produced.
2663
+ */
2664
+ function sofaErrorResponse(errors) {
2665
+ let status;
2666
+ const headers = { "Content-Type": "application/json; charset=utf-8" };
2667
+ for (const error of errors) {
2668
+ const http = error?.extensions?.http;
2669
+ if (!http) continue;
2670
+ if (http.status && (!status || http.status > status)) status = http.status;
2671
+ if (http.headers) Object.assign(headers, http.headers);
2672
+ delete error.extensions.http;
2673
+ }
2674
+ return Response.json({ errors }, {
2675
+ status: status ?? 500,
2676
+ headers
2677
+ });
2678
+ }
2449
2679
  const rumble = (rumbleInput) => {
2450
2680
  if (!rumbleInput.db._.relations) throw new RumbleError(`
2451
2681
  rumble could not find any relations in the provided drizzle instance.
@@ -2551,6 +2781,33 @@ export const r = rumble({
2551
2781
  types: filteredTypes
2552
2782
  });
2553
2783
  });
2784
+ /**
2785
+ * Adapts the configured rumble logger to the logger interface yoga expects,
2786
+ * so yoga's own diagnostics end up in the same structured stream (with the
2787
+ * same `rumble.transport` and trace correlation fields) as rumble's entries.
2788
+ */
2789
+ const yogaLogging = () => {
2790
+ const log = telemetryLogger(rumbleInput);
2791
+ if (!log) return void 0;
2792
+ const emit = (level) => (...args) => {
2793
+ const messages = args.filter((arg) => typeof arg === "string");
2794
+ const rest = args.filter((arg) => typeof arg !== "string");
2795
+ const errors = rest.filter((entry) => entry instanceof Error);
2796
+ const details = rest.filter((entry) => !(entry instanceof Error));
2797
+ log[level]({
2798
+ [ATTR_TRANSPORT]: "graphql",
2799
+ ...traceCorrelationFields(rumbleInput),
2800
+ ...errors.length > 0 ? errorsLogField(errors) : {},
2801
+ ...details.length > 0 ? { details } : {}
2802
+ }, messages.join(" ") || "yoga diagnostic");
2803
+ };
2804
+ return {
2805
+ debug: emit("debug"),
2806
+ info: emit("info"),
2807
+ warn: emit("warn"),
2808
+ error: emit("error")
2809
+ };
2810
+ };
2554
2811
  const createYoga$1 = (args) => {
2555
2812
  const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
2556
2813
  return createYoga({
@@ -2558,38 +2815,39 @@ export const r = rumble({
2558
2815
  graphiql: enableApiDocs,
2559
2816
  schema: builtSchema(),
2560
2817
  context,
2818
+ logging: args?.logging ?? yogaLogging() ?? true,
2561
2819
  plugins: [
2562
2820
  ...args?.plugins ?? [],
2563
2821
  ...enableApiDocs ? [] : [useDisableIntrospection(), EnvelopArmorPlugin(args?.armorConfig)],
2564
- rumbleInput.otel?.enabled || rumbleInput.logger?.enabled ? { onExecute: ({ setExecuteFn, executeFn }) => {
2565
- setExecuteFn(buildTracedExecute(executeFn, rumbleInput));
2566
- } } : false
2822
+ telemetryEnabled(rumbleInput) ? {
2823
+ onExecute: ({ setExecuteFn, executeFn }) => {
2824
+ setExecuteFn(buildTracedExecute(executeFn, rumbleInput, "graphql"));
2825
+ },
2826
+ onSubscribe: ({ setSubscribeFn, subscribeFn }) => {
2827
+ setSubscribeFn(buildTracedSubscribe(subscribeFn, rumbleInput, "graphql"));
2828
+ }
2829
+ } : false
2567
2830
  ].filter(Boolean)
2568
2831
  });
2569
2832
  };
2570
2833
  const createSofa = async (args) => {
2571
- const { useSofa: useSofaFn } = await import("sofa-api");
2834
+ const { useSofa: useSofaFn } = await import("@m1212e/sofa-api");
2572
2835
  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
- }
2836
+ const userErrorHandler = args.errorHandler;
2582
2837
  return useSofaFn({
2583
2838
  ...args,
2584
2839
  schema: builtSchema(),
2585
2840
  context,
2586
- execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput),
2587
- subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput),
2841
+ execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput, "rest"),
2842
+ subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput, "rest"),
2588
2843
  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 });
2844
+ recordSpanErrors(trace.getActiveSpan(), errors);
2845
+ telemetryLogger(rumbleInput)?.error({
2846
+ [ATTR_TRANSPORT]: "rest",
2847
+ ...traceCorrelationFields(rumbleInput),
2848
+ ...errorsLogField(errors)
2849
+ }, "rest request failed");
2850
+ return userErrorHandler?.(errors) ?? sofaErrorResponse(errors);
2593
2851
  }
2594
2852
  });
2595
2853
  };
@@ -2598,8 +2856,8 @@ export const r = rumble({
2598
2856
  ...args,
2599
2857
  schema: builtSchema(),
2600
2858
  context,
2601
- execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput),
2602
- subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput)
2859
+ execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput, "ws"),
2860
+ subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput, "ws")
2603
2861
  }, ...rest);
2604
2862
  };
2605
2863
  return {