@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 +530 -248
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +53 -3
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +54 -4
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +531 -249
- package/out/index.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
484
|
-
|
|
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
|
|
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 = (
|
|
964
|
+
const assembleAbilities = (attributes) => {
|
|
751
965
|
const filters = queryFilters.get(action);
|
|
752
966
|
if (filters === "unrestricted") {
|
|
753
|
-
|
|
967
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
754
968
|
return transformToResponse();
|
|
755
969
|
}
|
|
756
970
|
if (!filters) {
|
|
757
|
-
|
|
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")
|
|
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
|
-
|
|
772
|
-
|
|
988
|
+
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
989
|
+
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
773
990
|
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
774
|
-
|
|
991
|
+
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
775
992
|
if (allQueryFilters.length === 0) {
|
|
776
|
-
|
|
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
|
-
|
|
999
|
+
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
783
1000
|
return transformToResponse(mergedFilters);
|
|
784
1001
|
};
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1002
|
+
const run = (span) => {
|
|
1003
|
+
const attributes = {
|
|
1004
|
+
[ATTR_TABLE]: String(tableName),
|
|
1005
|
+
[ATTR_ACTION]: action
|
|
1006
|
+
};
|
|
788
1007
|
try {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
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/
|
|
1345
|
-
function
|
|
1346
|
-
return typeof
|
|
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
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
-
|
|
1361
|
-
|
|
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
|
-
*
|
|
1366
|
-
*
|
|
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
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
|
|
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)
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
|
1411
|
-
...
|
|
1412
|
-
|
|
1618
|
+
log?.info({
|
|
1619
|
+
...logFields,
|
|
1620
|
+
[FIELD_EVENT_COUNT]: eventCount
|
|
1413
1621
|
}, "graphql subscription completed");
|
|
1414
1622
|
} catch (error) {
|
|
1415
|
-
log
|
|
1416
|
-
...
|
|
1417
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
const
|
|
1428
|
-
const start =
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
|
1645
|
+
const result = await executeFn(options);
|
|
1451
1646
|
if (result && "errors" in result && result.errors?.length) {
|
|
1452
|
-
|
|
1453
|
-
|
|
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
|
-
...
|
|
1460
|
-
|
|
1661
|
+
...telemetry.logFields,
|
|
1662
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1461
1663
|
...errorLogField(error)
|
|
1462
1664
|
}, "graphql execute threw");
|
|
1463
|
-
span
|
|
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
|
-
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
const start =
|
|
1487
|
-
const
|
|
1488
|
-
|
|
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
|
-
...
|
|
1501
|
-
|
|
1502
|
-
}, "graphql
|
|
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
|
|
1698
|
+
const result = await subscribeFn(options);
|
|
1514
1699
|
if (!isAsyncIterable(result)) {
|
|
1515
1700
|
const execResult = result;
|
|
1516
1701
|
if (execResult.errors?.length) {
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
1526
|
-
|
|
1725
|
+
...telemetry.logFields,
|
|
1726
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1527
1727
|
...errorLogField(error)
|
|
1528
1728
|
}, "graphql subscribe threw");
|
|
1529
|
-
span
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
2357
|
-
span.setAttribute(
|
|
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(
|
|
2592
|
+
span?.setAttribute(ATTR_FILTERS_ALLOWED, allowed.length);
|
|
2369
2593
|
this.logger?.debug({
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
2378
|
-
|
|
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
|
-
|
|
2383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2594
|
-
setExecuteFn
|
|
2595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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 {
|