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