@m1212e/rumble 0.23.17 → 0.23.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/index.cjs +530 -248
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +53 -3
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +54 -4
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +531 -249
- package/out/index.mjs.map +1 -1
- package/package.json +1 -1
package/out/index.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.19";
|
|
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,182 @@ 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
|
+
/**
|
|
567
|
+
* Errors the operation wrapper has already reported.
|
|
568
|
+
*
|
|
569
|
+
* A transport can see the same failure twice: sofa, for instance, hands
|
|
570
|
+
* `result.errors` to its error handler *after* execute has returned, so without
|
|
571
|
+
* this the very same error would produce two log lines and two exception events,
|
|
572
|
+
* which quietly doubles error counts in dashboards. A WeakSet keeps this free of
|
|
573
|
+
* bookkeeping — entries disappear with the errors themselves.
|
|
574
|
+
*/
|
|
575
|
+
const reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
576
|
+
function markErrorsReported(errors) {
|
|
577
|
+
for (const error of errors) if (typeof error === "object" && error !== null) reportedErrors.add(error);
|
|
578
|
+
}
|
|
579
|
+
/** True when every error was already reported by the operation wrapper. */
|
|
580
|
+
function errorsAlreadyReported(errors) {
|
|
581
|
+
if (errors.length === 0) return false;
|
|
582
|
+
return errors.every((error) => typeof error === "object" && error !== null && reportedErrors.has(error));
|
|
583
|
+
}
|
|
584
|
+
function safeJsonStringify(value) {
|
|
585
|
+
try {
|
|
586
|
+
return JSON.stringify(value, (_key, inner) => typeof inner === "bigint" ? inner.toString() : inner) ?? "null";
|
|
587
|
+
} catch {
|
|
588
|
+
return String(value);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function isHomogeneousPrimitiveArray(value) {
|
|
592
|
+
let seen;
|
|
593
|
+
for (const entry of value) {
|
|
594
|
+
if (entry === null || entry === void 0) continue;
|
|
595
|
+
const type = typeof entry;
|
|
596
|
+
if (type !== "string" && type !== "number" && type !== "boolean") return false;
|
|
597
|
+
if (seen && seen !== type) return false;
|
|
598
|
+
seen = type;
|
|
599
|
+
}
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Coerces an arbitrary value into something the OpenTelemetry attribute type
|
|
604
|
+
* allows: primitives pass through, homogeneous primitive arrays stay arrays
|
|
605
|
+
* (mixed ones are invalid per spec) and everything else becomes JSON.
|
|
606
|
+
*
|
|
607
|
+
* Values are intentionally not truncated here — length limits belong to the SDK
|
|
608
|
+
* (`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`) so they stay configurable per deployment.
|
|
609
|
+
*/
|
|
610
|
+
function attributeValue(value) {
|
|
611
|
+
switch (typeof value) {
|
|
612
|
+
case "string": return value;
|
|
613
|
+
case "number": return Number.isFinite(value) ? value : String(value);
|
|
614
|
+
case "boolean": return value;
|
|
615
|
+
case "bigint": return value.toString();
|
|
616
|
+
case "undefined": return "null";
|
|
617
|
+
default: break;
|
|
618
|
+
}
|
|
619
|
+
if (value === null) return "null";
|
|
620
|
+
if (Array.isArray(value) && isHomogeneousPrimitiveArray(value)) return value;
|
|
621
|
+
return safeJsonStringify(value);
|
|
622
|
+
}
|
|
623
|
+
function resolveVariables(variables, include) {
|
|
624
|
+
if (include === false || !variables) return void 0;
|
|
625
|
+
const resolved = typeof include === "function" ? include({ ...variables }) : variables;
|
|
626
|
+
if (!resolved || Object.keys(resolved).length === 0) return void 0;
|
|
627
|
+
return resolved;
|
|
628
|
+
}
|
|
629
|
+
/** Variables flattened into `graphql.variables.<name>` span attributes. */
|
|
630
|
+
function variableAttributes(variables, include) {
|
|
631
|
+
const resolved = resolveVariables(variables, include);
|
|
632
|
+
if (!resolved) return {};
|
|
633
|
+
const attributes = {};
|
|
634
|
+
for (const [key, value] of Object.entries(resolved)) attributes[`${ATTR_VARIABLES_PREFIX}${key}`] = attributeValue(value);
|
|
635
|
+
return attributes;
|
|
636
|
+
}
|
|
637
|
+
/** Variables as a single nested `graphql.variables` log field. */
|
|
638
|
+
function variableLogField(variables, include) {
|
|
639
|
+
const resolved = resolveVariables(variables, include);
|
|
640
|
+
return resolved ? { [FIELD_VARIABLES]: resolved } : {};
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Monotonic clock reading. `Date.now()` can jump backwards when the wall clock is
|
|
644
|
+
* adjusted (NTP), which would produce negative or inflated durations.
|
|
645
|
+
*/
|
|
646
|
+
function startTimer() {
|
|
647
|
+
return performance.now();
|
|
648
|
+
}
|
|
649
|
+
function durationMs(start) {
|
|
650
|
+
return Math.round((performance.now() - start) * 100) / 100;
|
|
651
|
+
}
|
|
652
|
+
//#endregion
|
|
444
653
|
//#region lib/abilityBuilder.ts
|
|
445
654
|
function isDynamicQueryFilter(filter) {
|
|
446
655
|
return typeof filter === "function" && filter.constructor.name !== "AsyncFunction";
|
|
@@ -448,17 +657,22 @@ function isDynamicQueryFilter(filter) {
|
|
|
448
657
|
function isStaticQueryFilter(filter) {
|
|
449
658
|
return typeof filter !== "function";
|
|
450
659
|
}
|
|
451
|
-
const makeNothingRegisteredWarner = (logger) => debounce((model, action) => {
|
|
660
|
+
const makeNothingRegisteredWarner = (config, logger) => debounce((model, action) => {
|
|
452
661
|
const msg = `No abilities registered for ${model}/${action} — blocking everything. Register the ability or ignore this warning if intentional.`;
|
|
453
662
|
if (logger) logger.warn({
|
|
454
|
-
|
|
455
|
-
|
|
663
|
+
[ATTR_TABLE]: model,
|
|
664
|
+
[ATTR_ACTION]: action,
|
|
665
|
+
...traceCorrelationFields(config)
|
|
456
666
|
}, msg);
|
|
457
667
|
else console.warn(msg);
|
|
458
668
|
}, 1e3);
|
|
459
669
|
const createAbilityBuilder = ({ db, actions, defaultLimit, otel, logger: loggerConfig }) => {
|
|
460
670
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
461
|
-
const
|
|
671
|
+
const telemetryConfig = {
|
|
672
|
+
otel,
|
|
673
|
+
logger: loggerConfig
|
|
674
|
+
};
|
|
675
|
+
const nothingRegisteredWarningLogger = makeNothingRegisteredWarner(telemetryConfig, log);
|
|
462
676
|
const tableRelationNames = Object.keys(db.query).filter((name) => tableHelper({
|
|
463
677
|
db,
|
|
464
678
|
table: name
|
|
@@ -718,14 +932,14 @@ by: (explicitFilter) => {
|
|
|
718
932
|
}
|
|
719
933
|
return { withContext: (userContext) => {
|
|
720
934
|
return { filter: (action) => {
|
|
721
|
-
const assembleAbilities = (
|
|
935
|
+
const assembleAbilities = (attributes) => {
|
|
722
936
|
const filters = queryFilters.get(action);
|
|
723
937
|
if (filters === "unrestricted") {
|
|
724
|
-
|
|
938
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
725
939
|
return transformToResponse();
|
|
726
940
|
}
|
|
727
941
|
if (!filters) {
|
|
728
|
-
|
|
942
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
729
943
|
nothingRegisteredWarningLogger(String(tableName), action);
|
|
730
944
|
return transformToResponse(blockEverythingFilter);
|
|
731
945
|
}
|
|
@@ -734,58 +948,60 @@ by: (explicitFilter) => {
|
|
|
734
948
|
for (let i = 0; i < dynamicQueryFilters[action].length; i++) {
|
|
735
949
|
const func = dynamicQueryFilters[action][i];
|
|
736
950
|
const result = func(userContext);
|
|
737
|
-
if (result === "allow")
|
|
951
|
+
if (result === "allow") {
|
|
952
|
+
attributes[ATTR_ABILITIES_STATUS] = "unrestricted";
|
|
953
|
+
return transformToResponse();
|
|
954
|
+
}
|
|
738
955
|
if (result === void 0) continue;
|
|
739
956
|
dynamicResults[filtersReturned++] = result;
|
|
740
957
|
}
|
|
741
958
|
dynamicResults.length = filtersReturned;
|
|
742
|
-
|
|
743
|
-
|
|
959
|
+
attributes[ATTR_ABILITIES_DYNAMIC] = dynamicResults.length;
|
|
960
|
+
attributes[ATTR_ABILITIES_STATIC] = simpleQueryFilters[action].length;
|
|
744
961
|
const allQueryFilters = [...simpleQueryFilters[action], ...dynamicResults];
|
|
745
|
-
|
|
962
|
+
attributes[ATTR_ABILITIES_TOTAL] = allQueryFilters.length;
|
|
746
963
|
if (allQueryFilters.length === 0) {
|
|
747
|
-
|
|
964
|
+
attributes[ATTR_ABILITIES_STATUS] = "blocked_everything";
|
|
748
965
|
return transformToResponse(blockEverythingFilter);
|
|
749
966
|
}
|
|
750
967
|
const mergedFilters = allQueryFilters.length === 1 ? allQueryFilters[0] : allQueryFilters.reduce((a, b) => {
|
|
751
968
|
return mergeFilters(a, b, "OR");
|
|
752
969
|
});
|
|
753
|
-
|
|
970
|
+
attributes[ATTR_ABILITIES_STATUS] = "applied";
|
|
754
971
|
return transformToResponse(mergedFilters);
|
|
755
972
|
};
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
973
|
+
const run = (span) => {
|
|
974
|
+
const attributes = {
|
|
975
|
+
[ATTR_TABLE]: String(tableName),
|
|
976
|
+
[ATTR_ACTION]: action
|
|
977
|
+
};
|
|
759
978
|
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");
|
|
979
|
+
const result = assembleAbilities(attributes);
|
|
980
|
+
log?.debug({
|
|
981
|
+
...attributes,
|
|
982
|
+
...traceCorrelationFields(telemetryConfig, span)
|
|
983
|
+
}, "abilities prepared");
|
|
773
984
|
return result;
|
|
985
|
+
} catch (error) {
|
|
986
|
+
recordSpanError(span, error);
|
|
987
|
+
log?.error({
|
|
988
|
+
...attributes,
|
|
989
|
+
...traceCorrelationFields(telemetryConfig, span),
|
|
990
|
+
...errorLogField(error)
|
|
991
|
+
}, "abilities failed");
|
|
992
|
+
throw error;
|
|
993
|
+
} finally {
|
|
994
|
+
span?.setAttributes(attributes);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
if (otel?.enabled && otel.tracer) return otel.tracer.startActiveSpan(SPAN_ABILITIES_PREPARE, (span) => {
|
|
998
|
+
try {
|
|
999
|
+
return run(span);
|
|
774
1000
|
} finally {
|
|
775
1001
|
span.end();
|
|
776
1002
|
}
|
|
777
1003
|
});
|
|
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
|
-
}
|
|
1004
|
+
return run();
|
|
789
1005
|
} };
|
|
790
1006
|
} };
|
|
791
1007
|
};
|
|
@@ -1312,207 +1528,192 @@ const sofaOpenAPIWebhookDocs = {
|
|
|
1312
1528
|
} }
|
|
1313
1529
|
};
|
|
1314
1530
|
//#endregion
|
|
1315
|
-
//#region lib/helpers/
|
|
1316
|
-
function
|
|
1317
|
-
return typeof
|
|
1531
|
+
//#region lib/helpers/tracing.ts
|
|
1532
|
+
function isAsyncIterable(value) {
|
|
1533
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
1318
1534
|
}
|
|
1319
|
-
function
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1535
|
+
function buildOperationTelemetry(config, transport, options) {
|
|
1536
|
+
const operationAst = getOperationAST(options.document, options.operationName);
|
|
1537
|
+
const operationName = options.operationName ?? operationAst?.name?.value ?? "anonymous";
|
|
1538
|
+
const operationType = operationAst?.operation;
|
|
1539
|
+
const logFields = {
|
|
1540
|
+
[ATTR_OPERATION_NAME]: operationName,
|
|
1541
|
+
...operationType ? { [ATTR_OPERATION_TYPE]: operationType } : {},
|
|
1542
|
+
[ATTR_TRANSPORT]: transport
|
|
1543
|
+
};
|
|
1330
1544
|
return {
|
|
1331
|
-
|
|
1332
|
-
|
|
1545
|
+
operationName,
|
|
1546
|
+
operationType,
|
|
1547
|
+
logFields,
|
|
1548
|
+
variableLogFields: variableLogField(options.variableValues, config.logger?.includeVariables),
|
|
1549
|
+
attributes: () => ({
|
|
1550
|
+
...logFields,
|
|
1551
|
+
...config.otel?.includeDocument === false ? {} : { [ATTR_DOCUMENT]: print(options.document) },
|
|
1552
|
+
...variableAttributes(options.variableValues, config.otel?.includeVariables)
|
|
1553
|
+
})
|
|
1333
1554
|
};
|
|
1334
1555
|
}
|
|
1335
1556
|
/**
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
* or a GraphQL `ExecutionResult.errors` list (multi-element array).
|
|
1557
|
+
* Wraps a subscription's event stream so delivered events are observable no
|
|
1558
|
+
* matter which transport consumes them.
|
|
1339
1559
|
*/
|
|
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")
|
|
1560
|
+
async function* wrapSubscriptionIterator(iterator, telemetry) {
|
|
1561
|
+
const { log, tracer, setupSpanContext, logFields, attributes } = telemetry;
|
|
1562
|
+
const recordEventErrors = (eventCount, errors) => {
|
|
1563
|
+
if (!tracer) return;
|
|
1564
|
+
const span = tracer.startSpan(SPAN_SUBSCRIBE_EVENT, {
|
|
1565
|
+
root: true,
|
|
1566
|
+
links: setupSpanContext ? [{ context: setupSpanContext }] : void 0,
|
|
1567
|
+
attributes: {
|
|
1568
|
+
...attributes,
|
|
1569
|
+
[ATTR_SUBSCRIPTION_EVENT_INDEX]: eventCount
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
recordSpanErrors(span, errors);
|
|
1573
|
+
span.end();
|
|
1364
1574
|
};
|
|
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
1575
|
let eventCount = 0;
|
|
1371
1576
|
try {
|
|
1372
1577
|
for await (const event of iterator) {
|
|
1373
1578
|
eventCount++;
|
|
1374
|
-
if (event.errors?.length)
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1579
|
+
if (event.errors?.length) {
|
|
1580
|
+
log?.error({
|
|
1581
|
+
...logFields,
|
|
1582
|
+
[FIELD_EVENT_COUNT]: eventCount,
|
|
1583
|
+
...errorsLogField(event.errors)
|
|
1584
|
+
}, "graphql subscription event error");
|
|
1585
|
+
recordEventErrors(eventCount, event.errors);
|
|
1586
|
+
}
|
|
1379
1587
|
yield event;
|
|
1380
1588
|
}
|
|
1381
|
-
log
|
|
1382
|
-
...
|
|
1383
|
-
|
|
1589
|
+
log?.info({
|
|
1590
|
+
...logFields,
|
|
1591
|
+
[FIELD_EVENT_COUNT]: eventCount
|
|
1384
1592
|
}, "graphql subscription completed");
|
|
1385
1593
|
} catch (error) {
|
|
1386
|
-
log
|
|
1387
|
-
...
|
|
1388
|
-
|
|
1594
|
+
log?.error({
|
|
1595
|
+
...logFields,
|
|
1596
|
+
[FIELD_EVENT_COUNT]: eventCount,
|
|
1389
1597
|
...errorLogField(error)
|
|
1390
1598
|
}, "graphql subscription threw");
|
|
1599
|
+
recordEventErrors(eventCount, [error]);
|
|
1391
1600
|
throw error;
|
|
1392
1601
|
}
|
|
1393
1602
|
}
|
|
1394
|
-
function buildTracedExecute(executeFn, rumbleInput) {
|
|
1603
|
+
function buildTracedExecute(executeFn, rumbleInput, transport) {
|
|
1604
|
+
if (!telemetryEnabled(rumbleInput)) return executeFn;
|
|
1395
1605
|
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));
|
|
1606
|
+
const tracer = telemetryTracer(rumbleInput);
|
|
1607
|
+
let log = telemetryLogger(rumbleInput);
|
|
1608
|
+
const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
|
|
1609
|
+
const start = startTimer();
|
|
1610
|
+
const run = async (span) => {
|
|
1611
|
+
log?.info({
|
|
1612
|
+
...telemetry.logFields,
|
|
1613
|
+
...telemetry.variableLogFields
|
|
1614
|
+
}, "graphql execute start");
|
|
1420
1615
|
try {
|
|
1421
|
-
const result = await
|
|
1616
|
+
const result = await executeFn(options);
|
|
1422
1617
|
if (result && "errors" in result && result.errors?.length) {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1618
|
+
log?.error({
|
|
1619
|
+
...telemetry.logFields,
|
|
1620
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1621
|
+
...errorsLogField(result.errors)
|
|
1622
|
+
}, "graphql execute completed with errors");
|
|
1623
|
+
recordSpanErrors(span, result.errors);
|
|
1624
|
+
markErrorsReported(result.errors);
|
|
1625
|
+
} else log?.info({
|
|
1626
|
+
...telemetry.logFields,
|
|
1627
|
+
[FIELD_DURATION_MS]: durationMs(start)
|
|
1628
|
+
}, "graphql execute completed");
|
|
1426
1629
|
return result;
|
|
1427
1630
|
} catch (error) {
|
|
1428
|
-
if (error instanceof Error) span.recordException(error);
|
|
1429
1631
|
log?.error({
|
|
1430
|
-
...
|
|
1431
|
-
|
|
1632
|
+
...telemetry.logFields,
|
|
1633
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1432
1634
|
...errorLogField(error)
|
|
1433
1635
|
}, "graphql execute threw");
|
|
1434
|
-
span
|
|
1636
|
+
recordSpanError(span, error);
|
|
1637
|
+
markErrorsReported([error]);
|
|
1435
1638
|
throw error;
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
if (!tracer) return run();
|
|
1642
|
+
return tracer.startActiveSpan(SPAN_EXECUTE, { attributes: telemetry.attributes() }, async (span) => {
|
|
1643
|
+
if (log) {
|
|
1644
|
+
const correlation = traceCorrelationFields(rumbleInput, span);
|
|
1645
|
+
if (Object.keys(correlation).length > 0) log = log.child(correlation);
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
return await run(span);
|
|
1436
1649
|
} finally {
|
|
1437
1650
|
span.end();
|
|
1438
1651
|
}
|
|
1439
1652
|
});
|
|
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
1653
|
};
|
|
1451
1654
|
}
|
|
1452
|
-
function buildTracedSubscribe(subscribeFn, rumbleInput) {
|
|
1655
|
+
function buildTracedSubscribe(subscribeFn, rumbleInput, transport) {
|
|
1656
|
+
if (!telemetryEnabled(rumbleInput)) return subscribeFn;
|
|
1453
1657
|
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
|
-
}
|
|
1658
|
+
const tracer = telemetryTracer(rumbleInput);
|
|
1659
|
+
let log = telemetryLogger(rumbleInput);
|
|
1660
|
+
const telemetry = buildOperationTelemetry(rumbleInput, transport, options);
|
|
1661
|
+
const start = startTimer();
|
|
1662
|
+
const attributes = tracer ? telemetry.attributes() : {};
|
|
1663
|
+
const doSubscribe = async (span) => {
|
|
1470
1664
|
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));
|
|
1665
|
+
...telemetry.logFields,
|
|
1666
|
+
...telemetry.variableLogFields
|
|
1667
|
+
}, "graphql subscribe start");
|
|
1483
1668
|
try {
|
|
1484
|
-
const result = await
|
|
1669
|
+
const result = await subscribeFn(options);
|
|
1485
1670
|
if (!isAsyncIterable(result)) {
|
|
1486
1671
|
const execResult = result;
|
|
1487
1672
|
if (execResult.errors?.length) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1673
|
+
log?.error({
|
|
1674
|
+
...telemetry.logFields,
|
|
1675
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1676
|
+
...errorsLogField(execResult.errors)
|
|
1677
|
+
}, "graphql subscribe completed with errors");
|
|
1678
|
+
recordSpanErrors(span, execResult.errors);
|
|
1679
|
+
markErrorsReported(execResult.errors);
|
|
1490
1680
|
}
|
|
1681
|
+
return execResult;
|
|
1491
1682
|
}
|
|
1492
|
-
|
|
1683
|
+
log?.info({
|
|
1684
|
+
...telemetry.logFields,
|
|
1685
|
+
[FIELD_DURATION_MS]: durationMs(start)
|
|
1686
|
+
}, "graphql subscription established");
|
|
1687
|
+
return wrapSubscriptionIterator(result, {
|
|
1688
|
+
log,
|
|
1689
|
+
tracer,
|
|
1690
|
+
setupSpanContext: span?.spanContext(),
|
|
1691
|
+
logFields: telemetry.logFields,
|
|
1692
|
+
attributes
|
|
1693
|
+
});
|
|
1493
1694
|
} catch (error) {
|
|
1494
|
-
if (error instanceof Error) span.recordException(error);
|
|
1495
1695
|
log?.error({
|
|
1496
|
-
...
|
|
1497
|
-
|
|
1696
|
+
...telemetry.logFields,
|
|
1697
|
+
[FIELD_DURATION_MS]: durationMs(start),
|
|
1498
1698
|
...errorLogField(error)
|
|
1499
1699
|
}, "graphql subscribe threw");
|
|
1500
|
-
span
|
|
1700
|
+
recordSpanError(span, error);
|
|
1701
|
+
markErrorsReported([error]);
|
|
1501
1702
|
throw error;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
if (!tracer) return doSubscribe();
|
|
1706
|
+
return tracer.startActiveSpan(SPAN_SUBSCRIBE, { attributes }, async (span) => {
|
|
1707
|
+
if (log) {
|
|
1708
|
+
const correlation = traceCorrelationFields(rumbleInput, span);
|
|
1709
|
+
if (Object.keys(correlation).length > 0) log = log.child(correlation);
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
return await doSubscribe(span);
|
|
1502
1713
|
} finally {
|
|
1503
1714
|
span.end();
|
|
1504
1715
|
}
|
|
1505
1716
|
});
|
|
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
1717
|
};
|
|
1517
1718
|
}
|
|
1518
1719
|
//#endregion
|
|
@@ -1661,17 +1862,30 @@ const isProbablyAConfigObject = (t) => {
|
|
|
1661
1862
|
].find((e) => e === k))) return true;
|
|
1662
1863
|
return false;
|
|
1663
1864
|
};
|
|
1664
|
-
const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
|
|
1865
|
+
const createObjectImplementer = ({ db, search, otel, logger: loggerConfig, schemaBuilder, makePubSubInstance, whereArgImplementer, orderArgImplementer, enumImplementer, abilityBuilder }) => {
|
|
1866
|
+
const telemetryConfig = {
|
|
1867
|
+
otel,
|
|
1868
|
+
logger: loggerConfig
|
|
1869
|
+
};
|
|
1870
|
+
/**
|
|
1871
|
+
* Schema construction warnings go through the configured logger when there is
|
|
1872
|
+
* one, so they end up in the same structured stream as everything else, and
|
|
1873
|
+
* fall back to the console otherwise.
|
|
1874
|
+
*/
|
|
1875
|
+
const warn = (msg, fields = {}) => {
|
|
1876
|
+
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
1877
|
+
if (log) log.warn({
|
|
1878
|
+
...fields,
|
|
1879
|
+
...traceCorrelationFields(telemetryConfig)
|
|
1880
|
+
}, msg);
|
|
1881
|
+
else console.warn(msg);
|
|
1882
|
+
};
|
|
1665
1883
|
return ({ table, refName, readAction = "read", adjust }) => {
|
|
1666
1884
|
const tableSchema = tableHelper({
|
|
1667
1885
|
db,
|
|
1668
1886
|
table
|
|
1669
1887
|
});
|
|
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
|
-
}
|
|
1888
|
+
if (Object.keys(tableSchema.primaryKey).length === 0) warn(`Could not find primary key for ${String(table)}. Cannot register subscriptions!`, { [ATTR_TABLE]: String(table) });
|
|
1675
1889
|
const primaryKey = Object.values(tableSchema.primaryKey)[0];
|
|
1676
1890
|
const { registerOnInstance } = makePubSubInstance({ table });
|
|
1677
1891
|
return schemaBuilder.drizzleObject(table, {
|
|
@@ -1680,9 +1894,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
|
|
|
1680
1894
|
if (!primaryKey) return;
|
|
1681
1895
|
const primaryKeyValue = element[primaryKey.name];
|
|
1682
1896
|
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);
|
|
1897
|
+
warn(`Could not find primary key value for element on ${String(table)}. Cannot register subscription!`, { [ATTR_TABLE]: String(table) });
|
|
1686
1898
|
return;
|
|
1687
1899
|
}
|
|
1688
1900
|
registerOnInstance({
|
|
@@ -1714,7 +1926,7 @@ const createObjectImplementer = ({ db, search, logger: loggerConfig, schemaBuild
|
|
|
1714
1926
|
} })) ?? {};
|
|
1715
1927
|
const fields = Object.entries(columns).reduce((acc, [key, column]) => {
|
|
1716
1928
|
if (key.startsWith("__")) {
|
|
1717
|
-
|
|
1929
|
+
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
1930
|
return acc;
|
|
1719
1931
|
}
|
|
1720
1932
|
if (userAdjustments[key]) {
|
|
@@ -1917,8 +2129,12 @@ const createPubSubInstance = ({ subscriptions }) => {
|
|
|
1917
2129
|
};
|
|
1918
2130
|
//#endregion
|
|
1919
2131
|
//#region lib/query.ts
|
|
1920
|
-
const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
|
|
2132
|
+
const createQueryImplementer = ({ db, schemaBuilder, search, otel, logger: loggerConfig, whereArgImplementer, orderArgImplementer, makePubSubInstance }) => {
|
|
1921
2133
|
const registeredQueryFieldNames = /* @__PURE__ */ new Set();
|
|
2134
|
+
const telemetryConfig = {
|
|
2135
|
+
otel,
|
|
2136
|
+
logger: loggerConfig
|
|
2137
|
+
};
|
|
1922
2138
|
return ({ table, readAction = "read", listAction = "read", findFirst, findMany }) => {
|
|
1923
2139
|
const WhereArg = whereArgImplementer({ table });
|
|
1924
2140
|
const OrderArg = orderArgImplementer({ table });
|
|
@@ -1990,7 +2206,7 @@ const createQueryImplementer = ({ db, schemaBuilder, search, logger: loggerConfi
|
|
|
1990
2206
|
else {
|
|
1991
2207
|
const msg = "Database dialect is not postgresql, cannot set cpu_operator_cost.";
|
|
1992
2208
|
const log = loggerConfig?.enabled ? loggerConfig.logger : void 0;
|
|
1993
|
-
log ? log.info(
|
|
2209
|
+
log ? log.info(traceCorrelationFields(telemetryConfig), msg) : console.info(msg);
|
|
1994
2210
|
}
|
|
1995
2211
|
return run(tx);
|
|
1996
2212
|
});
|
|
@@ -2282,6 +2498,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2282
2498
|
tracer;
|
|
2283
2499
|
tracerEnabled;
|
|
2284
2500
|
logger;
|
|
2501
|
+
telemetryConfig = {};
|
|
2285
2502
|
filterLoaders = /* @__PURE__ */ new WeakMap();
|
|
2286
2503
|
getLoader(context, filter) {
|
|
2287
2504
|
let perContext = this.filterLoaders.get(context);
|
|
@@ -2310,6 +2527,10 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2310
2527
|
this.tracer = this.builder.options.otel?.tracer;
|
|
2311
2528
|
this.tracerEnabled = this.builder.options.otel?.enabled;
|
|
2312
2529
|
this.logger = this.builder.options.logger?.enabled ? this.builder.options.logger.logger : void 0;
|
|
2530
|
+
this.telemetryConfig = {
|
|
2531
|
+
otel: this.builder.options.otel,
|
|
2532
|
+
logger: this.builder.options.logger
|
|
2533
|
+
};
|
|
2313
2534
|
return typeConfig;
|
|
2314
2535
|
}
|
|
2315
2536
|
wrapResolve(resolver, fieldConfig) {
|
|
@@ -2321,13 +2542,16 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2321
2542
|
if (!filters || !Array.isArray(filters) || filters.length === 0) return resolver(parent, args, context, info);
|
|
2322
2543
|
const runFilters = async (span) => {
|
|
2323
2544
|
const allFilters = Array.isArray(filters) ? filters : [filters];
|
|
2324
|
-
span?.setAttribute(
|
|
2545
|
+
span?.setAttribute(ATTR_FILTERS_TOTAL, allFilters.length);
|
|
2325
2546
|
const loaders = allFilters.map((filter) => this.getLoader(context, filter));
|
|
2326
2547
|
let resolved;
|
|
2327
|
-
if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(
|
|
2328
|
-
span.setAttribute(
|
|
2548
|
+
if (this.tracer && this.tracerEnabled) resolved = await this.tracer.startActiveSpan(SPAN_FILTERS_RESOLVE, async (span) => {
|
|
2549
|
+
span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
|
|
2329
2550
|
try {
|
|
2330
2551
|
return await resolver(parent, args, context, info);
|
|
2552
|
+
} catch (error) {
|
|
2553
|
+
recordSpanError(span, error);
|
|
2554
|
+
throw error;
|
|
2331
2555
|
} finally {
|
|
2332
2556
|
span.end();
|
|
2333
2557
|
}
|
|
@@ -2336,24 +2560,30 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2336
2560
|
const entities = Array.isArray(resolved) ? resolved : [resolved];
|
|
2337
2561
|
const perFilterResults = await Promise.all(loaders.map((loader) => Promise.all(entities.map((entity) => loader.load(entity)))));
|
|
2338
2562
|
const allowed = entities.filter((_, index) => perFilterResults.some((results) => results[index] != null));
|
|
2339
|
-
span?.setAttribute(
|
|
2563
|
+
span?.setAttribute(ATTR_FILTERS_ALLOWED, allowed.length);
|
|
2340
2564
|
this.logger?.debug({
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2565
|
+
[ATTR_FIELD_NAME]: fieldConfig.name,
|
|
2566
|
+
[ATTR_FILTERS_TOTAL]: allFilters.length,
|
|
2567
|
+
[ATTR_FILTERS_ALLOWED]: allowed.length,
|
|
2568
|
+
...traceCorrelationFields(this.telemetryConfig)
|
|
2344
2569
|
}, "runtime filters applied");
|
|
2345
2570
|
if (Array.isArray(resolved)) return allowed;
|
|
2346
2571
|
return allowed[0] ?? null;
|
|
2347
2572
|
};
|
|
2348
|
-
|
|
2349
|
-
|
|
2573
|
+
const logFilterError = (error) => {
|
|
2574
|
+
this.logger?.error({
|
|
2575
|
+
[ATTR_FIELD_NAME]: fieldConfig.name,
|
|
2576
|
+
...traceCorrelationFields(this.telemetryConfig),
|
|
2577
|
+
...errorLogField(error)
|
|
2578
|
+
}, "runtime filter threw");
|
|
2579
|
+
};
|
|
2580
|
+
if (this.tracer && this.tracerEnabled) return this.tracer.startActiveSpan(SPAN_FILTERS_APPLY, async (span) => {
|
|
2581
|
+
span.setAttribute(ATTR_FIELD_NAME, fieldConfig.name);
|
|
2350
2582
|
try {
|
|
2351
2583
|
return await runFilters(span);
|
|
2352
2584
|
} catch (error) {
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
...errorLogField(error)
|
|
2356
|
-
}, "runtime filter threw");
|
|
2585
|
+
logFilterError(error);
|
|
2586
|
+
recordSpanError(span, error);
|
|
2357
2587
|
throw error;
|
|
2358
2588
|
} finally {
|
|
2359
2589
|
span.end();
|
|
@@ -2362,10 +2592,7 @@ var RuntimeFiltersPlugin = class extends BasePlugin {
|
|
|
2362
2592
|
else try {
|
|
2363
2593
|
return await runFilters();
|
|
2364
2594
|
} catch (error) {
|
|
2365
|
-
|
|
2366
|
-
"graphql.field.name": fieldConfig.name,
|
|
2367
|
-
...errorLogField(error)
|
|
2368
|
-
}, "runtime filter threw");
|
|
2595
|
+
logFilterError(error);
|
|
2369
2596
|
throw error;
|
|
2370
2597
|
}
|
|
2371
2598
|
};
|
|
@@ -2413,18 +2640,22 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2413
2640
|
let r = createSpan ? createSpan(resolver, options) : resolver;
|
|
2414
2641
|
if (logger?.enabled) {
|
|
2415
2642
|
const log = logger.logger;
|
|
2643
|
+
const telemetryConfig = {
|
|
2644
|
+
otel,
|
|
2645
|
+
logger
|
|
2646
|
+
};
|
|
2416
2647
|
r = wrapResolver(r, (error, duration) => {
|
|
2648
|
+
const fields = {
|
|
2649
|
+
[ATTR_FIELD_NAME]: config.name,
|
|
2650
|
+
[ATTR_PARENT_TYPE]: config.parentType,
|
|
2651
|
+
[FIELD_DURATION_MS]: duration,
|
|
2652
|
+
...traceCorrelationFields(telemetryConfig)
|
|
2653
|
+
};
|
|
2417
2654
|
if (error) log.error({
|
|
2418
|
-
|
|
2419
|
-
"graphql.parent.type": config.parentType,
|
|
2420
|
-
duration_ms: duration,
|
|
2655
|
+
...fields,
|
|
2421
2656
|
...errorLogField(error)
|
|
2422
2657
|
}, "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");
|
|
2658
|
+
else log.debug(fields, "resolver completed");
|
|
2428
2659
|
});
|
|
2429
2660
|
}
|
|
2430
2661
|
return r;
|
|
@@ -2446,6 +2677,27 @@ const createSchemaBuilder = ({ db, pubsub, pothosConfig, otel, logger }) => {
|
|
|
2446
2677
|
};
|
|
2447
2678
|
//#endregion
|
|
2448
2679
|
//#region lib/rumble.ts
|
|
2680
|
+
/**
|
|
2681
|
+
* Mirrors sofa's own default error handler: it derives the HTTP status from
|
|
2682
|
+
* `extensions.http` and returns the errors as JSON. rumble only adds telemetry
|
|
2683
|
+
* around error handling, so a request that fails must still answer with the
|
|
2684
|
+
* status and body a plain sofa setup would have produced.
|
|
2685
|
+
*/
|
|
2686
|
+
function sofaErrorResponse(errors) {
|
|
2687
|
+
let status;
|
|
2688
|
+
const headers = { "Content-Type": "application/json; charset=utf-8" };
|
|
2689
|
+
for (const error of errors) {
|
|
2690
|
+
const http = error?.extensions?.http;
|
|
2691
|
+
if (!http) continue;
|
|
2692
|
+
if (http.status && (!status || http.status > status)) status = http.status;
|
|
2693
|
+
if (http.headers) Object.assign(headers, http.headers);
|
|
2694
|
+
delete error.extensions.http;
|
|
2695
|
+
}
|
|
2696
|
+
return Response.json({ errors }, {
|
|
2697
|
+
status: status ?? 500,
|
|
2698
|
+
headers
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2449
2701
|
const rumble = (rumbleInput) => {
|
|
2450
2702
|
if (!rumbleInput.db._.relations) throw new RumbleError(`
|
|
2451
2703
|
rumble could not find any relations in the provided drizzle instance.
|
|
@@ -2551,6 +2803,33 @@ export const r = rumble({
|
|
|
2551
2803
|
types: filteredTypes
|
|
2552
2804
|
});
|
|
2553
2805
|
});
|
|
2806
|
+
/**
|
|
2807
|
+
* Adapts the configured rumble logger to the logger interface yoga expects,
|
|
2808
|
+
* so yoga's own diagnostics end up in the same structured stream (with the
|
|
2809
|
+
* same `rumble.transport` and trace correlation fields) as rumble's entries.
|
|
2810
|
+
*/
|
|
2811
|
+
const yogaLogging = () => {
|
|
2812
|
+
const log = telemetryLogger(rumbleInput);
|
|
2813
|
+
if (!log) return void 0;
|
|
2814
|
+
const emit = (level) => (...args) => {
|
|
2815
|
+
const messages = args.filter((arg) => typeof arg === "string");
|
|
2816
|
+
const rest = args.filter((arg) => typeof arg !== "string");
|
|
2817
|
+
const errors = rest.filter((entry) => entry instanceof Error);
|
|
2818
|
+
const details = rest.filter((entry) => !(entry instanceof Error));
|
|
2819
|
+
log[level]({
|
|
2820
|
+
[ATTR_TRANSPORT]: "graphql",
|
|
2821
|
+
...traceCorrelationFields(rumbleInput),
|
|
2822
|
+
...errors.length > 0 ? errorsLogField(errors) : {},
|
|
2823
|
+
...details.length > 0 ? { details } : {}
|
|
2824
|
+
}, messages.join(" ") || "yoga diagnostic");
|
|
2825
|
+
};
|
|
2826
|
+
return {
|
|
2827
|
+
debug: emit("debug"),
|
|
2828
|
+
info: emit("info"),
|
|
2829
|
+
warn: emit("warn"),
|
|
2830
|
+
error: emit("error")
|
|
2831
|
+
};
|
|
2832
|
+
};
|
|
2554
2833
|
const createYoga$1 = (args) => {
|
|
2555
2834
|
const enableApiDocs = args?.enableApiDocs ?? process?.env?.NODE_ENV === "development";
|
|
2556
2835
|
return createYoga({
|
|
@@ -2558,38 +2837,41 @@ export const r = rumble({
|
|
|
2558
2837
|
graphiql: enableApiDocs,
|
|
2559
2838
|
schema: builtSchema(),
|
|
2560
2839
|
context,
|
|
2840
|
+
logging: args?.logging ?? yogaLogging() ?? true,
|
|
2561
2841
|
plugins: [
|
|
2562
2842
|
...args?.plugins ?? [],
|
|
2563
2843
|
...enableApiDocs ? [] : [useDisableIntrospection(), EnvelopArmorPlugin(args?.armorConfig)],
|
|
2564
|
-
rumbleInput
|
|
2565
|
-
setExecuteFn
|
|
2566
|
-
|
|
2844
|
+
telemetryEnabled(rumbleInput) ? {
|
|
2845
|
+
onExecute: ({ setExecuteFn, executeFn }) => {
|
|
2846
|
+
setExecuteFn(buildTracedExecute(executeFn, rumbleInput, "graphql"));
|
|
2847
|
+
},
|
|
2848
|
+
onSubscribe: ({ setSubscribeFn, subscribeFn }) => {
|
|
2849
|
+
setSubscribeFn(buildTracedSubscribe(subscribeFn, rumbleInput, "graphql"));
|
|
2850
|
+
}
|
|
2851
|
+
} : false
|
|
2567
2852
|
].filter(Boolean)
|
|
2568
2853
|
});
|
|
2569
2854
|
};
|
|
2570
2855
|
const createSofa = async (args) => {
|
|
2571
2856
|
const { useSofa: useSofaFn } = await import("@m1212e/sofa-api");
|
|
2572
2857
|
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
|
-
}
|
|
2858
|
+
const userErrorHandler = args.errorHandler;
|
|
2582
2859
|
return useSofaFn({
|
|
2583
2860
|
...args,
|
|
2584
2861
|
schema: builtSchema(),
|
|
2585
2862
|
context,
|
|
2586
|
-
execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput),
|
|
2587
|
-
subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput),
|
|
2863
|
+
execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput, "rest"),
|
|
2864
|
+
subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput, "rest"),
|
|
2588
2865
|
errorHandler(errors) {
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2866
|
+
if (!errorsAlreadyReported(errors)) {
|
|
2867
|
+
recordSpanErrors(trace.getActiveSpan(), errors);
|
|
2868
|
+
telemetryLogger(rumbleInput)?.error({
|
|
2869
|
+
[ATTR_TRANSPORT]: "rest",
|
|
2870
|
+
...traceCorrelationFields(rumbleInput),
|
|
2871
|
+
...errorsLogField(errors)
|
|
2872
|
+
}, "rest request failed");
|
|
2873
|
+
}
|
|
2874
|
+
return userErrorHandler?.(errors) ?? sofaErrorResponse(errors);
|
|
2593
2875
|
}
|
|
2594
2876
|
});
|
|
2595
2877
|
};
|
|
@@ -2598,8 +2880,8 @@ export const r = rumble({
|
|
|
2598
2880
|
...args,
|
|
2599
2881
|
schema: builtSchema(),
|
|
2600
2882
|
context,
|
|
2601
|
-
execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput),
|
|
2602
|
-
subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput)
|
|
2883
|
+
execute: buildTracedExecute(args.execute ?? defaultExecute, rumbleInput, "ws"),
|
|
2884
|
+
subscribe: buildTracedSubscribe(args.subscribe ?? defaultSubscribe, rumbleInput, "ws")
|
|
2603
2885
|
}, ...rest);
|
|
2604
2886
|
};
|
|
2605
2887
|
return {
|