@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.cjs +507 -249
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +165 -18
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +166 -19
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +508 -250
- package/out/index.mjs.map +1 -1
- package/package.json +6 -6
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.18";
|
|
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,164 @@ 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
|
+
function safeJsonStringify(value) {
|
|
596
|
+
try {
|
|
597
|
+
return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner) ?? "null";
|
|
598
|
+
} catch {
|
|
599
|
+
return String(value);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function isHomogeneousPrimitiveArray(value) {
|
|
603
|
+
let seen;
|
|
604
|
+
for (const entry of value) {
|
|
605
|
+
if (entry === null || entry === void 0) continue;
|
|
606
|
+
const type = typeof entry;
|
|
607
|
+
if (type !== "string" && type !== "number" && type !== "boolean") return false;
|
|
608
|
+
if (seen && seen !== type) return false;
|
|
609
|
+
seen = type;
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Coerces an arbitrary value into something the OpenTelemetry attribute type
|
|
615
|
+
* allows: primitives pass through, homogeneous primitive arrays stay arrays
|
|
616
|
+
* (mixed ones are invalid per spec) and everything else becomes JSON.
|
|
617
|
+
*
|
|
618
|
+
* Values are intentionally not truncated here — length limits belong to the SDK
|
|
619
|
+
* (`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`) so they stay configurable per deployment.
|
|
620
|
+
*/
|
|
621
|
+
function attributeValue(value) {
|
|
622
|
+
switch (typeof value) {
|
|
623
|
+
case "string": return value;
|
|
624
|
+
case "number": return Number.isFinite(value) ? value : String(value);
|
|
625
|
+
case "boolean": return value;
|
|
626
|
+
case "bigint": return value.toString();
|
|
627
|
+
case "undefined": return "null";
|
|
628
|
+
default: break;
|
|
629
|
+
}
|
|
630
|
+
if (value === null) return "null";
|
|
631
|
+
if (Array.isArray(value) && isHomogeneousPrimitiveArray(value)) return value;
|
|
632
|
+
return safeJsonStringify(value);
|
|
633
|
+
}
|
|
634
|
+
function resolveVariables(variables, include) {
|
|
635
|
+
if (include === false || !variables) return void 0;
|
|
636
|
+
const resolved = typeof include === "function" ? include({ ...variables }) : variables;
|
|
637
|
+
if (!resolved || Object.keys(resolved).length === 0) return void 0;
|
|
638
|
+
return resolved;
|
|
639
|
+
}
|
|
640
|
+
/** Variables flattened into `graphql.variables.<name>` span attributes. */
|
|
641
|
+
function variableAttributes(variables, include) {
|
|
642
|
+
const resolved = resolveVariables(variables, include);
|
|
643
|
+
if (!resolved) return {};
|
|
644
|
+
const attributes = {};
|
|
645
|
+
for (const [key, value] of Object.entries(resolved)) attributes[`${ATTR_VARIABLES_PREFIX}${key}`] = attributeValue(value);
|
|
646
|
+
return attributes;
|
|
647
|
+
}
|
|
648
|
+
/** Variables as a single nested `graphql.variables` log field. */
|
|
649
|
+
function variableLogField(variables, include) {
|
|
650
|
+
const resolved = resolveVariables(variables, include);
|
|
651
|
+
return resolved ? { [FIELD_VARIABLES]: resolved } : {};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Monotonic clock reading. `Date.now()` can jump backwards when the wall clock is
|
|
655
|
+
* adjusted (NTP), which would produce negative or inflated durations.
|
|
656
|
+
*/
|
|
657
|
+
function startTimer() {
|
|
658
|
+
return performance.now();
|
|
659
|
+
}
|
|
660
|
+
function durationMs(start) {
|
|
661
|
+
return Math.round((performance.now() - start) * 100) / 100;
|
|
662
|
+
}
|
|
663
|
+
//#endregion
|
|
473
664
|
//#region lib/abilityBuilder.ts
|
|
474
665
|
function isDynamicQueryFilter(filter) {
|
|
475
666
|
return typeof filter === "function" && filter.constructor.name !== "AsyncFunction";
|
|
@@ -477,17 +668,22 @@ function isDynamicQueryFilter(filter) {
|
|
|
477
668
|
function isStaticQueryFilter(filter) {
|
|
478
669
|
return typeof filter !== "function";
|
|
479
670
|
}
|
|
480
|
-
const makeNothingRegisteredWarner = (logger) => (0, es_toolkit.debounce)((model, action) => {
|
|
671
|
+
const makeNothingRegisteredWarner = (config, logger) => (0, es_toolkit.debounce)((model, action) => {
|
|
481
672
|
const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
|
|
482
673
|
if (logger) logger.warn({
|
|
483
|
-
|
|
484
|
-
|
|
674
|
+
[ATTR_TABLE]: model,
|
|
675
|
+
[ATTR_ACTION]: action,
|
|
676
|
+
...traceCorrelationFields(config)
|
|
485
677
|
}, msg);
|
|
486
678
|
else console.warn(msg);
|
|
487
679
|
}, 1e3);
|
|
488
680
|
const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
|
|
489
681
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
490
|
-
const
|
|
682
|
+
const telemetryConfig = {
|
|
683
|
+
otel,
|
|
684
|
+
logger: loggerConfig
|
|
685
|
+
};
|
|
686
|
+
const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
|
|
491
687
|
const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
|
|
492
688
|
db,
|
|
493
689
|
table: name
|
|
@@ -747,14 +943,14 @@ by: (explicitFilter) => {
|
|
|
747
943
|
}
|
|
748
944
|
return { withContext: (userContext) => {
|
|
749
945
|
return { filter: (action) => {
|
|
750
|
-
const assembleAbilities = (
|
|
946
|
+
const assembleAbilities = (attributes) => {
|
|
751
947
|
const filters = queryFilters.get(action);
|
|
752
948
|
if (filters === "unrestricted") {
|
|
753
|
-
|
|
949
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
754
950
|
return transformToResponse();
|
|
755
951
|
}
|
|
756
952
|
if (!filters) {
|
|
757
|
-
|
|
953
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
758
954
|
nothingRegisteredWarningLogger(String(tableName), action);
|
|
759
955
|
return transformToResponse(blockEverythingFilter);
|
|
760
956
|
}
|
|
@@ -763,58 +959,60 @@ by: (explicitFilter) => {
|
|
|
763
959
|
for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
|
|
764
960
|
const func = dynamicQueryFilters[action][i];
|
|
765
961
|
const result = func(userContext);
|
|
766
|
-
if (result === "allow")
|
|
962
|
+
if (result === "allow") {
|
|
963
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
964
|
+
return transformToResponse();
|
|
965
|
+
}
|
|
767
966
|
if (result === void 0) continue;
|
|
768
967
|
dynamicResults[filtersReturned++] = result;
|
|
769
968
|
}
|
|
770
969
|
dynamicResults.length = filtersReturned;
|
|
771
|
-
|
|
772
|
-
|
|
970
|
+
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
971
|
+
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
773
972
|
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
774
|
-
|
|
973
|
+
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
775
974
|
if (allQueryFilters.length === 0) {
|
|
776
|
-
|
|
975
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
777
976
|
return transformToResponse(blockEverythingFilter);
|
|
778
977
|
}
|
|
779
978
|
const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
|
|
780
979
|
return mergeFilters(a, b, "OR");
|
|
781
980
|
});
|
|
782
|
-
|
|
981
|
+
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
783
982
|
return transformToResponse(mergedFilters);
|
|
784
983
|
};
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
984
|
+
const run = (span) => {
|
|
985
|
+
const attributes = {
|
|
986
|
+
[ATTR_TABLE]: String(tableName),
|
|
987
|
+
[ATTR_ACTION]: action
|
|
988
|
+
};
|
|
788
989
|
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");
|
|
990
|
+
const result = assembleAbilities(attributes);
|
|
991
|
+
log?.debug({
|
|
992
|
+
...attributes,
|
|
993
|
+
...traceCorrelationFields(telemetryConfig, span)
|
|
994
|
+
}, "abilities prepared");
|
|
802
995
|
return result;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
recordSpanError(span, error);
|
|
998
|
+
log?.error({
|
|
999
|
+
...attributes,
|
|
1000
|
+
...traceCorrelationFields(telemetryConfig, span),
|
|
1001
|
+
...errorLogField(error)
|
|
1002
|
+
}, "abilities failed");
|
|
1003
|
+
throw error;
|
|
1004
|
+
} finally {
|
|
1005
|
+
span?.setAttributes(attributes);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, (span) => {
|
|
1009
|
+
try {
|
|
1010
|
+
return run(span);
|
|
803
1011
|
} finally {
|
|
804
1012
|
span.end();
|
|
805
1013
|
}
|
|
806
1014
|
});
|
|
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
|
-
}
|
|
1015
|
+
return run();
|
|
818
1016
|
} };
|
|
819
1017
|
} };
|
|
820
1018
|
};
|
|
@@ -1341,207 +1539,188 @@ const sofaOpenAPIWebhookDocs = {
|
|
|
1341
1539
|
} }
|
|
1342
1540
|
};
|
|
1343
1541
|
//#endregion
|
|
1344
|
-
//#region lib/helpers/
|
|
1345
|
-
function
|
|
1346
|
-
return typeof
|
|
1542
|
+
//#region lib/helpers/tracing.ts
|
|
1543
|
+
function isAsyncIterable(value) {
|
|
1544
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
1347
1545
|
}
|
|
1348
|
-
function
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1546
|
+
function buildOperationTelemetry(config, transport, options) {
|
|
1547
|
+
const operationAst = (0, graphql.getOperationAST)(options.document, options.operationName);
|
|
1548
|
+
const operationName = options.operationName ?? operationAst?.name?.value ?? "anonymous";
|
|
1549
|
+
const operationType = operationAst?.operation;
|
|
1550
|
+
const logFields = {
|
|
1551
|
+
[ATTR_OPERATION_NAME]: operationName,
|
|
1552
|
+
...operationType ? { [ATTR_OPERATION_TYPE]: operationType } : {},
|
|
1553
|
+
[ATTR_TRANSPORT]: transport
|
|
1554
|
+
};
|
|
1359
1555
|
return {
|
|
1360
|
-
|
|
1361
|
-
|
|
1556
|
+
operationName,
|
|
1557
|
+
operationType,
|
|
1558
|
+
logFields,
|
|
1559
|
+
variableLogFields: variableLogField(options.variableValues, config.logger?.includeVariables),
|
|
1560
|
+
attributes: () => ({
|
|
1561
|
+
...logFields,
|
|
1562
|
+
...config.otel?.includeDocument === false ? {} : { [ATTR_DOCUMENT]: (0, graphql.print)(options.document) },
|
|
1563
|
+
...variableAttributes(options.variableValues, config.otel?.includeVariables)
|
|
1564
|
+
})
|
|
1362
1565
|
};
|
|
1363
1566
|
}
|
|
1364
1567
|
/**
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
* or a GraphQL `ExecutionResult.errors` list (multi-element array).
|
|
1568
|
+
* Wraps a subscription's event stream so delivered events are observable no
|
|
1569
|
+
* matter which transport consumes them.
|
|
1368
1570
|
*/
|
|
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")
|
|
1571
|
+
async function* wrapSubscriptionIterator(iterator, telemetry) {
|
|
1572
|
+
const { log, tracer, setupSpanContext, logFields, attributes } = telemetry;
|
|
1573
|
+
const recordEventErrors = (eventCount, errors) => {
|
|
1574
|
+
if (!tracer) return;
|
|
1575
|
+
const span = tracer.startSpan(SPAN_SUBSCRIBE_EVENT, {
|
|
1576
|
+
root: true,
|
|
1577
|
+
links: setupSpanContext ? [{ context: setupSpanContext }] : void 0,
|
|
1578
|
+
attributes: {
|
|
1579
|
+
...attributes,
|
|
1580
|
+
[ATTR_SUBSCRIPTION_EVENT_INDEX]: eventCount
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
recordSpanErrors(span, errors);
|
|
1584
|
+
span.end();
|
|
1393
1585
|
};
|
|
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
1586
|
let eventCount = 0;
|
|
1400
1587
|
try {
|
|
1401
1588
|
for await (const event of iterator) {
|
|
1402
1589
|
eventCount++;
|
|
1403
|
-
if (event.errors?.length)
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1590
|
+
if (event.errors?.length) {
|
|
1591
|
+
log?.error({
|
|
1592
|
+
...logFields,
|
|
1593
|
+
[FIELD_EVENT_COUNT]: eventCount,
|
|
1594
|
+
...errorsLogField(event.errors)
|
|
1595
|
+
}, "graphql subscription event error");
|
|
1596
|
+
recordEventErrors(eventCount, event.errors);
|
|
1597
|
+
}
|
|
1408
1598
|
yield event;
|
|
1409
1599
|
}
|
|
1410
|
-
log
|
|
1411
|
-
...
|
|
1412
|
-
|
|
1600
|
+
log?.info({
|
|
1601
|
+
...logFields,
|
|
1602
|
+
[FIELD_EVENT_COUNT]: eventCount
|
|
1413
1603
|
}, "graphql subscription completed");
|
|
1414
1604
|
} catch (error) {
|
|
1415
|
-
log
|
|
1416
|
-
...
|
|
1417
|
-
|
|
1605
|
+
log?.error({
|
|
1606
|
+
...logFields,
|
|
1607
|
+
[FIELD_EVENT_COUNT]: eventCount,
|
|
1418
1608
|
...errorLogField(error)
|
|
1419
1609
|
}, "graphql subscription threw");
|
|
1610
|
+
recordEventErrors(eventCount, [error]);
|
|
1420
1611
|
throw error;
|
|
1421
1612
|
}
|
|
1422
1613
|
}
|
|
1423
|
-
function buildTracedExecute(executeFn, rumbleInput) {
|
|
1614
|
+
function buildTracedExecute(executeFn, rumbleInput, transport) {
|
|
1615
|
+
if (!telemetryEnabled(rumbleInput)) return executeFn;
|
|
1424
1616
|
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));
|
|
1617
|
+
const tracer = telemetryTracer(rumbleInput);
|
|
1618
|
+
let log = telemetryLogger(rumbleInput);
|
|
1619
|
+
const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
|
|
1620
|
+
const start = startTimer();
|
|
1621
|
+
const run = async (span) => {
|
|
1622
|
+
log?.info({
|
|
1623
|
+
...telemetry.logFields,
|
|
1624
|
+
...telemetry.variableLogFields
|
|
1625
|
+
}, "graphql execute start");
|
|
1449
1626
|
try {
|
|
1450
|
-
const result = await
|
|
1627
|
+
const result = await executeFn(options);
|
|
1451
1628
|
if (result && "errors" in result && result.errors?.length) {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1629
|
+
log?.error({
|
|
1630
|
+
...telemetry.logFields,
|
|
1631
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1632
|
+
...errorsLogField(result.errors)
|
|
1633
|
+
}, "graphql execute completed with errors");
|
|
1634
|
+
recordSpanErrors(span, result.errors);
|
|
1635
|
+
} else log?.info({
|
|
1636
|
+
...telemetry.logFields,
|
|
1637
|
+
[FIELD_DURATION_MS]: durationMs(start)
|
|
1638
|
+
}, "graphql execute completed");
|
|
1455
1639
|
return result;
|
|
1456
1640
|
} catch (error) {
|
|
1457
|
-
if (error instanceof Error) span.recordException(error);
|
|
1458
1641
|
log?.error({
|
|
1459
|
-
...
|
|
1460
|
-
|
|
1642
|
+
...telemetry.logFields,
|
|
1643
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1461
1644
|
...errorLogField(error)
|
|
1462
1645
|
}, "graphql execute threw");
|
|
1463
|
-
span
|
|
1646
|
+
recordSpanError(span, error);
|
|
1464
1647
|
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
if (!tracer) return run();
|
|
1651
|
+
return tracer.startActiveSpan(SPAN_EXECUTE, { attributes: telemetry.attributes() }, async (span) => {
|
|
1652
|
+
if (log) {
|
|
1653
|
+
const correlation = traceCorrelationFields(rumbleInput, span);
|
|
1654
|
+
if (Object.keys(correlation).length > 0) log = log.child(correlation);
|
|
1655
|
+
}
|
|
1656
|
+
try {
|
|
1657
|
+
return await run(span);
|
|
1465
1658
|
} finally {
|
|
1466
1659
|
span.end();
|
|
1467
1660
|
}
|
|
1468
1661
|
});
|
|
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
1662
|
};
|
|
1480
1663
|
}
|
|
1481
|
-
function buildTracedSubscribe(subscribeFn, rumbleInput) {
|
|
1664
|
+
function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
|
|
1665
|
+
if (!telemetryEnabled(rumbleInput)) return subscribeFn;
|
|
1482
1666
|
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
|
-
}
|
|
1667
|
+
const tracer = telemetryTracer(rumbleInput);
|
|
1668
|
+
let log = telemetryLogger(rumbleInput);
|
|
1669
|
+
const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
|
|
1670
|
+
const start = startTimer();
|
|
1671
|
+
const attributes = tracer ? telemetry.attributes() : {};
|
|
1672
|
+
const doSubscribe = async (span) => {
|
|
1499
1673
|
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));
|
|
1674
|
+
...telemetry.logFields,
|
|
1675
|
+
...telemetry.variableLogFields
|
|
1676
|
+
}, "graphql subscribe start");
|
|
1512
1677
|
try {
|
|
1513
|
-
const result = await
|
|
1678
|
+
const result = await subscribeFn(options);
|
|
1514
1679
|
if (!isAsyncIterable(result)) {
|
|
1515
1680
|
const execResult = result;
|
|
1516
1681
|
if (execResult.errors?.length) {
|
|
1517
|
-
|
|
1518
|
-
|
|
1682
|
+
log?.error({
|
|
1683
|
+
...telemetry.logFields,
|
|
1684
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1685
|
+
...errorsLogField(execResult.errors)
|
|
1686
|
+
}, "graphql subscribe completed with errors");
|
|
1687
|
+
recordSpanErrors(span, execResult.errors);
|
|
1519
1688
|
}
|
|
1689
|
+
return execResult;
|
|
1520
1690
|
}
|
|
1521
|
-
|
|
1691
|
+
log?.info({
|
|
1692
|
+
...telemetry.logFields,
|
|
1693
|
+
[FIELD_DURATION_MS]: durationMs(start)
|
|
1694
|
+
}, "graphql subscription established");
|
|
1695
|
+
return wrapSubscriptionIterator(result, {
|
|
1696
|
+
log,
|
|
1697
|
+
tracer,
|
|
1698
|
+
setupSpanContext: span?.spanContext(),
|
|
1699
|
+
logFields: telemetry.logFields,
|
|
1700
|
+
attributes
|
|
1701
|
+
});
|
|
1522
1702
|
} catch (error) {
|
|
1523
|
-
if (error instanceof Error) span.recordException(error);
|
|
1524
1703
|
log?.error({
|
|
1525
|
-
...
|
|
1526
|
-
|
|
1704
|
+
...telemetry.logFields,
|
|
1705
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1527
1706
|
...errorLogField(error)
|
|
1528
1707
|
}, "graphql subscribe threw");
|
|
1529
|
-
span
|
|
1708
|
+
recordSpanError(span, error);
|
|
1530
1709
|
throw error;
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
if (!tracer) return doSubscribe();
|
|
1713
|
+
return tracer.startActiveSpan(SPAN_SUBSCRIBE, { attributes }, async (span) => {
|
|
1714
|
+
if (log) {
|
|
1715
|
+
const correlation = traceCorrelationFields(rumbleInput, span);
|
|
1716
|
+
if (Object.keys(correlation).length > 0) log = log.child(correlation);
|
|
1717
|
+
}
|
|
1718
|
+
try {
|
|
1719
|
+
return await doSubscribe(span);
|
|
1531
1720
|
} finally {
|
|
1532
1721
|
span.end();
|
|
1533
1722
|
}
|
|
1534
1723
|
});
|
|
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
1724
|
};
|
|
1546
1725
|
}
|
|
1547
1726
|
//#endregion
|
|
@@ -1690,17 +1869,30 @@ const isProbablyAConfigObject = (t) => {
|
|
|
1690
1869
|
].find((e) => e === k))) return true;
|
|
1691
1870
|
return false;
|
|
1692
1871
|
};
|
|
1693
|
-
const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
|
|
1872
|
+
const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
|
|
1873
|
+
const telemetryConfig = {
|
|
1874
|
+
otel,
|
|
1875
|
+
logger: loggerConfig
|
|
1876
|
+
};
|
|
1877
|
+
/**
|
|
1878
|
+
* Schema construction warnings go through the configured logger when there is
|
|
1879
|
+
* one, so they end up in the same structured stream as everything else, and
|
|
1880
|
+
* fall back to the console otherwise.
|
|
1881
|
+
*/
|
|
1882
|
+
const warn = (msg, fields = {}) => {
|
|
1883
|
+
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
1884
|
+
if (log) log.warn({
|
|
1885
|
+
...fields,
|
|
1886
|
+
...traceCorrelationFields(telemetryConfig)
|
|
1887
|
+
}, msg);
|
|
1888
|
+
else console.warn(msg);
|
|
1889
|
+
};
|
|
1694
1890
|
return ({ table, refName, readAction = "read", adjust }) => {
|
|
1695
1891
|
const tableSchema = tableHelper({
|
|
1696
1892
|
db,
|
|
1697
1893
|
table
|
|
1698
1894
|
});
|
|
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
|
-
}
|
|
1895
|
+
if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
|
|
1704
1896
|
const primaryKey = Object.values(tableSchema.primaryKey)[0];
|
|
1705
1897
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
1706
1898
|
return schemaBuilder.drizzleObject(table, {
|
|
@@ -1709,9 +1901,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
|
|
|
1709
1901
|
if (!primaryKey) return;
|
|
1710
1902
|
const primaryKeyValue = element[primaryKey.name];
|
|
1711
1903
|
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);
|
|
1904
|
+
warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
|
|
1715
1905
|
return;
|
|
1716
1906
|
}
|
|
1717
1907
|
registerOnInstance({
|
|
@@ -1743,7 +1933,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
|
|
|
1743
1933
|
} })) ?? {};
|
|
1744
1934
|
const fields = Object.entries(columns).reduce((acc, [key, column]) => {
|
|
1745
1935
|
if (key.startsWith("__")) {
|
|
1746
|
-
|
|
1936
|
+
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
1937
|
return acc;
|
|
1748
1938
|
}
|
|
1749
1939
|
if (userAdjustments[key]) {
|
|
@@ -1946,8 +2136,12 @@ const createPubSubInstance = ({ subscriptions }) => {
|
|
|
1946
2136
|
};
|
|
1947
2137
|
//#endregion
|
|
1948
2138
|
//#region lib/query.ts
|
|
1949
|
-
const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
|
|
2139
|
+
const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
|
|
1950
2140
|
const registeredQueryFieldNames = /* @__PURE__ */ new Set();
|
|
2141
|
+
const telemetryConfig = {
|
|
2142
|
+
otel,
|
|
2143
|
+
logger: loggerConfig
|
|
2144
|
+
};
|
|
1951
2145
|
return ({ table, readAction = "read", listAction = "read", findFirst, findMany }) => {
|
|
1952
2146
|
const WhereArg = whereArgImplementer({ table });
|
|
1953
2147
|
const OrderArg = orderArgImplementer({ table });
|
|
@@ -2019,7 +2213,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfi
|
|
|
2019
2213
|
else {
|
|
2020
2214
|
const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
|
|
2021
2215
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
2022
|
-
log ? log.info(
|
|
2216
|
+
log ? log.info(traceCorrelationFields(telemetryConfig), msg) : console.info(msg);
|
|
2023
2217
|
}
|
|
2024
2218
|
return run(tx);
|
|
2025
2219
|
});
|
|
@@ -2311,6 +2505,7 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2311
2505
|
tracer;
|
|
2312
2506
|
tracerEnabled;
|
|
2313
2507
|
logger;
|
|
2508
|
+
telemetryConfig = {};
|
|
2314
2509
|
filterLoaders = /* @__PURE__ */ new WeakMap();
|
|
2315
2510
|
getLoader(context, filter) {
|
|
2316
2511
|
let perContext = this.filterLoaders.get(context);
|
|
@@ -2339,6 +2534,10 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2339
2534
|
this.tracer = this.builder.options.otel?.tracer;
|
|
2340
2535
|
this.tracerEnabled = this.builder.options.otel?.enabled;
|
|
2341
2536
|
this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
|
|
2537
|
+
this.telemetryConfig = {
|
|
2538
|
+
otel: this.builder.options.otel,
|
|
2539
|
+
logger: this.builder.options.logger
|
|
2540
|
+
};
|
|
2342
2541
|
return typeConfig;
|
|
2343
2542
|
}
|
|
2344
2543
|
wrapResolve(resolver, fieldConfig) {
|
|
@@ -2350,13 +2549,16 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2350
2549
|
if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
|
|
2351
2550
|
const runFilters = async (span) => {
|
|
2352
2551
|
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2353
|
-
span?.setAttribute(
|
|
2552
|
+
span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
|
|
2354
2553
|
const loaders = allFilters.map((filter) => this.getLoader(context, filter));
|
|
2355
2554
|
let resolved;
|
|
2356
|
-
if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(
|
|
2357
|
-
span.setAttribute(
|
|
2555
|
+
if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(SPAN_FILTERS_RESOLVE, async (span) => {
|
|
2556
|
+
span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
|
|
2358
2557
|
try {
|
|
2359
2558
|
return await resolver(parent, args, context, info);
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
recordSpanError(span, error);
|
|
2561
|
+
throw error;
|
|
2360
2562
|
} finally {
|
|
2361
2563
|
span.end();
|
|
2362
2564
|
}
|
|
@@ -2365,24 +2567,30 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2365
2567
|
const entities = Array.isArray(resolved) ? resolved : [resolved];
|
|
2366
2568
|
const perFilterResults = await Promise.all(loaders.map((loader) => Promise.all(entities.map((entity) => loader.load(entity)))));
|
|
2367
2569
|
const allowed = entities.filter((_, index) => perFilterResults.some((results) => results[index] != null));
|
|
2368
|
-
span?.setAttribute(
|
|
2570
|
+
span?.setAttribute(ATTR_FILTERS_ALLOWED, allowed.length);
|
|
2369
2571
|
this.logger?.debug({
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2572
|
+
[ATTR_FIELD_NAME]: fieldConfig.name,
|
|
2573
|
+
[ATTR_FILTERS_TOTAL]: allFilters.length,
|
|
2574
|
+
[ATTR_FILTERS_ALLOWED]: allowed.length,
|
|
2575
|
+
...traceCorrelationFields(this.telemetryConfig)
|
|
2373
2576
|
}, "runtime filters applied");
|
|
2374
2577
|
if (Array.isArray(resolved)) return allowed;
|
|
2375
2578
|
return allowed[0] ?? null;
|
|
2376
2579
|
};
|
|
2377
|
-
|
|
2378
|
-
|
|
2580
|
+
const logFilterError = (error) => {
|
|
2581
|
+
this.logger?.error({
|
|
2582
|
+
[ATTR_FIELD_NAME]: fieldConfig.name,
|
|
2583
|
+
...traceCorrelationFields(this.telemetryConfig),
|
|
2584
|
+
...errorLogField(error)
|
|
2585
|
+
}, "runtime filter threw");
|
|
2586
|
+
};
|
|
2587
|
+
if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(SPAN_FILTERS_APPLY, async (span) => {
|
|
2588
|
+
span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
|
|
2379
2589
|
try {
|
|
2380
2590
|
return await runFilters(span);
|
|
2381
2591
|
} catch (error) {
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
...errorLogField(error)
|
|
2385
|
-
}, "runtime filter threw");
|
|
2592
|
+
logFilterError(error);
|
|
2593
|
+
recordSpanError(span, error);
|
|
2386
2594
|
throw error;
|
|
2387
2595
|
} finally {
|
|
2388
2596
|
span.end();
|
|
@@ -2391,10 +2599,7 @@ var RuntimeFiltersPlugin = class extends _pothos_core.BasePlugin {
|
|
|
2391
2599
|
else try {
|
|
2392
2600
|
return await runFilters();
|
|
2393
2601
|
} catch (error) {
|
|
2394
|
-
|
|
2395
|
-
"graphql.field.name": fieldConfig.name,
|
|
2396
|
-
...errorLogField(error)
|
|
2397
|
-
}, "runtime filter threw");
|
|
2602
|
+
logFilterError(error);
|
|
2398
2603
|
throw error;
|
|
2399
2604
|
}
|
|
2400
2605
|
};
|
|
@@ -2442,18 +2647,22 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2442
2647
|
let r = createSpan ? createSpan(resolver, options) : resolver;
|
|
2443
2648
|
if (logger?.enabled) {
|
|
2444
2649
|
const log = logger.logger;
|
|
2650
|
+
const telemetryConfig = {
|
|
2651
|
+
otel,
|
|
2652
|
+
logger
|
|
2653
|
+
};
|
|
2445
2654
|
r = (0, _pothos_plugin_tracing.wrapResolver)(r, (error, duration) => {
|
|
2655
|
+
const fields = {
|
|
2656
|
+
[ATTR_FIELD_NAME]: config.name,
|
|
2657
|
+
[ATTR_PARENT_TYPE]: config.parentType,
|
|
2658
|
+
[FIELD_DURATION_MS]: duration,
|
|
2659
|
+
...traceCorrelationFields(telemetryConfig)
|
|
2660
|
+
};
|
|
2446
2661
|
if (error) log.error({
|
|
2447
|
-
|
|
2448
|
-
"graphql.parent.type": config.parentType,
|
|
2449
|
-
duration_ms: duration,
|
|
2662
|
+
...fields,
|
|
2450
2663
|
...errorLogField(error)
|
|
2451
2664
|
}, "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");
|
|
2665
|
+
else log.debug(fields, "resolver completed");
|
|
2457
2666
|
});
|
|
2458
2667
|
}
|
|
2459
2668
|
return r;
|
|
@@ -2475,6 +2684,27 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2475
2684
|
};
|
|
2476
2685
|
//#endregion
|
|
2477
2686
|
//#region lib/rumble.ts
|
|
2687
|
+
/**
|
|
2688
|
+
* Mirrors sofa's own default error handler: it derives the HTTP status from
|
|
2689
|
+
* `extensions.http` and returns the errors as JSON. rumble only adds telemetry
|
|
2690
|
+
* around error handling, so a request that fails must still answer with the
|
|
2691
|
+
* status and body a plain sofa setup would have produced.
|
|
2692
|
+
*/
|
|
2693
|
+
function sofaErrorResponse(errors) {
|
|
2694
|
+
let status;
|
|
2695
|
+
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
|
2696
|
+
for (const error of errors) {
|
|
2697
|
+
const http = error?.extensions?.http;
|
|
2698
|
+
if (!http) continue;
|
|
2699
|
+
if (http.status && (!status || http.status > status)) status = http.status;
|
|
2700
|
+
if (http.headers) Object.assign(headers, http.headers);
|
|
2701
|
+
delete error.extensions.http;
|
|
2702
|
+
}
|
|
2703
|
+
return Response.json({ errors }, {
|
|
2704
|
+
status: status ?? 500,
|
|
2705
|
+
headers
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2478
2708
|
const rumble = (rumbleInput) => {
|
|
2479
2709
|
if (!rumbleInput.db._.relations) throw new RumbleError(`
|
|
2480
2710
|
rumble could not find any relations in the provided drizzle instance.
|
|
@@ -2580,6 +2810,33 @@ export const r = rumble({
|
|
|
2580
2810
|
types: filteredTypes
|
|
2581
2811
|
});
|
|
2582
2812
|
});
|
|
2813
|
+
/**
|
|
2814
|
+
* Adapts the configured rumble logger to the logger interface yoga expects,
|
|
2815
|
+
* so yoga's own diagnostics end up in the same structured stream (with the
|
|
2816
|
+
* same `rumble.transport` and trace correlation fields) as rumble's entries.
|
|
2817
|
+
*/
|
|
2818
|
+
const yogaLogging = () => {
|
|
2819
|
+
const log = telemetryLogger(rumbleInput);
|
|
2820
|
+
if (!log) return void 0;
|
|
2821
|
+
const emit = (level) => (...args) => {
|
|
2822
|
+
const messages = args.filter((arg) => typeof arg === "string");
|
|
2823
|
+
const rest = args.filter((arg) => typeof arg !== "string");
|
|
2824
|
+
const errors = rest.filter((entry) => entry instanceof Error);
|
|
2825
|
+
const details = rest.filter((entry) => !(entry instanceof Error));
|
|
2826
|
+
log[level]({
|
|
2827
|
+
[ATTR_TRANSPORT]: "graphql",
|
|
2828
|
+
...traceCorrelationFields(rumbleInput),
|
|
2829
|
+
...errors.length > 0 ? errorsLogField(errors) : {},
|
|
2830
|
+
...details.length > 0 ? { details } : {}
|
|
2831
|
+
}, messages.join(" ") || "yoga diagnostic");
|
|
2832
|
+
};
|
|
2833
|
+
return {
|
|
2834
|
+
debug: emit("debug"),
|
|
2835
|
+
info: emit("info"),
|
|
2836
|
+
warn: emit("warn"),
|
|
2837
|
+
error: emit("error")
|
|
2838
|
+
};
|
|
2839
|
+
};
|
|
2583
2840
|
const createYoga = (args) => {
|
|
2584
2841
|
const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
|
|
2585
2842
|
return (0, graphql_yoga.createYoga)({
|
|
@@ -2587,38 +2844,39 @@ export const r = rumble({
|
|
|
2587
2844
|
graphiql: enableApiDocs,
|
|
2588
2845
|
schema: builtSchema(),
|
|
2589
2846
|
context,
|
|
2847
|
+
logging: args?.logging ?? yogaLogging() ?? true,
|
|
2590
2848
|
plugins: [
|
|
2591
2849
|
...args?.plugins ?? [],
|
|
2592
2850
|
...enableApiDocs ? [] : [(0, _graphql_yoga_plugin_disable_introspection.useDisableIntrospection)(), (0, _escape_tech_graphql_armor.EnvelopArmorPlugin)(args?.armorConfig)],
|
|
2593
|
-
rumbleInput
|
|
2594
|
-
setExecuteFn
|
|
2595
|
-
|
|
2851
|
+
telemetryEnabled(rumbleInput) ? {
|
|
2852
|
+
onExecute: ({ setExecuteFn, executeFn }) => {
|
|
2853
|
+
setExecuteFn(buildTracedExecute(executeFn, rumbleInput, "graphql"));
|
|
2854
|
+
},
|
|
2855
|
+
onSubscribe: ({ setSubscribeFn, subscribeFn }) => {
|
|
2856
|
+
setSubscribeFn(buildTracedSubscribe(subscribeFn, rumbleInput, "graphql"));
|
|
2857
|
+
}
|
|
2858
|
+
} : false
|
|
2596
2859
|
].filter(Boolean)
|
|
2597
2860
|
});
|
|
2598
2861
|
};
|
|
2599
2862
|
const createSofa = async (args) => {
|
|
2600
|
-
const { useSofa: useSofaFn } = await import("sofa-api");
|
|
2863
|
+
const { useSofa: useSofaFn } = await import("@m1212e/sofa-api");
|
|
2601
2864
|
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
|
-
}
|
|
2865
|
+
const userErrorHandler = args.errorHandler;
|
|
2611
2866
|
return useSofaFn({
|
|
2612
2867
|
...args,
|
|
2613
2868
|
schema: builtSchema(),
|
|
2614
2869
|
context,
|
|
2615
|
-
execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput),
|
|
2616
|
-
subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput),
|
|
2870
|
+
execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput, "rest"),
|
|
2871
|
+
subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput, "rest"),
|
|
2617
2872
|
errorHandler(errors) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2873
|
+
recordSpanErrors(_opentelemetry_api.trace.getActiveSpan(), errors);
|
|
2874
|
+
telemetryLogger(rumbleInput)?.error({
|
|
2875
|
+
[ATTR_TRANSPORT]: "rest",
|
|
2876
|
+
...traceCorrelationFields(rumbleInput),
|
|
2877
|
+
...errorsLogField(errors)
|
|
2878
|
+
}, "rest request failed");
|
|
2879
|
+
return userErrorHandler?.(errors) ?? sofaErrorResponse(errors);
|
|
2622
2880
|
}
|
|
2623
2881
|
});
|
|
2624
2882
|
};
|
|
@@ -2627,8 +2885,8 @@ export const r = rumble({
|
|
|
2627
2885
|
...args,
|
|
2628
2886
|
schema: builtSchema(),
|
|
2629
2887
|
context,
|
|
2630
|
-
execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput),
|
|
2631
|
-
subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput)
|
|
2888
|
+
execute: buildTracedExecute(args.execute ?? graphql.execute, rumbleInput, "ws"),
|
|
2889
|
+
subscribe: buildTracedSubscribe(args.subscribe ?? graphql.subscribe, rumbleInput, "ws")
|
|
2632
2890
|
}, ...rest);
|
|
2633
2891
|
};
|
|
2634
2892
|
return {
|