Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposing the delegation plan for debugging purposes #6145

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/ten-eggs-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@graphql-tools/delegate": patch
"@graphql-tools/stitch": patch
---

Debugging the delegation plan;

After you enable the environment variable, `EXPOSE_DELEGATION_PLAN`, you can see the delegation plan in the console. This can be useful for debugging and understanding how the delegation works.

Also you can pass a different logger to it by using `logFnForContext` map from `@graphql-tools/delegate` package.

```ts
import { logFnForContext } from '@graphql-tools/delegate';
logFnForContext.set(MyGraphQLContext, console.log);
```

You can also add a `contextId` for that specific gateway request by using `contextIdMap` map from `@graphql-tools/delegate` package.

```ts
import { contextIdMap } from '@graphql-tools/delegate';
contextIdMap.set(MyGraphQLContext, 'my-request-id');
```

If you want to use those information instead of logging, you can get them by using the `context` object like below;

```ts
import { delegationPlanInfosByContext } from '@graphql-tools/delegate';
const delegationPlanInfos = delegationPlanInfosByContext.get(context);
```

35 changes: 35 additions & 0 deletions packages/delegate/src/debugging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SelectionSetNode } from 'graphql';
import { Subschema } from './Subschema.js';

export interface DelegationPlanInfo {
contextId?: string;
operationName?: string;
planId: string;
source?: string;
type?: string;
path: (string | number)[];
fieldNodes?: string[];
fragments: string[];
stages: {
stageId: string;
delegations: {
target?: string;
selectionSet: string;
}[];
}[];
}

export const delegationPlanIdMap = new WeakMap<
Map<Subschema<any, any, any, Record<string, any>>, SelectionSetNode>[],
string
>();
export const delegationStageIdMap = new WeakMap<
Map<Subschema<any, any, any, Record<string, any>>, SelectionSetNode>,
string
>();
export const logFnForContext = new WeakMap<any, (data: any) => void>();
export const delegationPlanInfosByContext = new WeakMap<any, Set<DelegationPlanInfo>>();
export const contextIdMap = new WeakMap<any, string>();
export function isDelegationDebugging() {
return globalThis.process?.env['EXPOSE_DELEGATION_PLAN'];
}
1 change: 1 addition & 0 deletions packages/delegate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './mergeFields.js';
export * from './resolveExternalValue.js';
export * from './subschemaConfig.js';
export * from './types.js';
export * from './debugging.js';
72 changes: 71 additions & 1 deletion packages/delegate/src/mergeFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GraphQLResolveInfo,
GraphQLSchema,
locatedError,
print,
responsePathAsArray,
SelectionSetNode,
} from 'graphql';
Expand All @@ -16,6 +17,15 @@ import {
mergeDeep,
relocatedError,
} from '@graphql-tools/utils';
import {
contextIdMap,
delegationPlanIdMap,
DelegationPlanInfo,
delegationPlanInfosByContext,
delegationStageIdMap,
isDelegationDebugging,
logFnForContext,
} from './debugging.js';
import { Subschema } from './Subschema.js';
import {
FIELD_SUBSCHEMA_MAP_SYMBOL,
Expand Down Expand Up @@ -83,9 +93,54 @@ export function mergeFields<TContext>(
: EMPTY_ARRAY,
);

let logFn: ((data: any) => void) | undefined;
if (isDelegationDebugging()) {
logFn = logFnForContext.get(context);
const delegationPlanInfo: DelegationPlanInfo = {
contextId: contextIdMap.get(context),
operationName: info.operation.name?.value,
planId: delegationPlanIdMap.get(delegationMaps)!,
source: sourceSubschema.name,
type: mergedTypeInfo.typeName,
path: responsePathAsArray(info.path),
fieldNodes: info.fieldNodes?.map(print),
fragments: Object.values(info.fragments || {}).map(fragmentNode => `${print(fragmentNode)}`),
stages: delegationMaps.map(delegationMap => ({
stageId: delegationStageIdMap.get(delegationMap)!,
delegations: Array.from(delegationMap).map(([subschema, selectionSet]) => ({
target: subschema.name,
selectionSet: print(selectionSet),
})),
})),
};
let delegationPlanInfos = delegationPlanInfosByContext.get(context);
if (!delegationPlanInfos) {
delegationPlanInfos = new Set();
delegationPlanInfosByContext.set(context, delegationPlanInfos);
}
delegationPlanInfos.add(delegationPlanInfo);
logFn?.({
status: 'PLAN',
plan: delegationPlanInfo,
});
}

const res$ = delegationMaps.reduce<MaybePromise<void>>((prev, delegationMap) => {
function executeFn() {
return executeDelegationStage(mergedTypeInfo, delegationMap, object, context, info);
return executeDelegationStage(
mergedTypeInfo,
delegationMap,
object,
context,
info,
logFn
? (data: any) =>
logFn({
stageId: delegationStageIdMap.get(delegationMap)!,
...data,
})
: undefined,
);
}
if (isPromise(prev)) {
return prev.then(executeFn);
Expand Down Expand Up @@ -170,6 +225,7 @@ function executeDelegationStage(
object: ExternalObject,
context: any,
info: GraphQLResolveInfo,
logFn?: (data: any) => void,
): MaybePromise<void> {
const combinedErrors = object[UNPATHED_ERRORS_SYMBOL];

Expand All @@ -178,9 +234,23 @@ function executeDelegationStage(
const combinedFieldSubschemaMap = object[FIELD_SUBSCHEMA_MAP_SYMBOL];

const jobs: PromiseLike<any>[] = [];
let delegationIndex = -1;
for (const [subschema, selectionSet] of delegationMap) {
const schema = subschema.transformedSchema || info.schema;
const type = schema.getType(object.__typename) as GraphQLObjectType;
if (logFn) {
delegationIndex++;
logFn({
status: 'EXECUTE_DELEGATION',
subschema: subschema.name,
typeName: type.name || mergedTypeInfo.typeName,
path,
selectionSet: print(selectionSet),
contextId: contextIdMap.get(context),
stageId: delegationStageIdMap.get(delegationMap)!,
delegationIndex,
});
}
const resolver = mergedTypeInfo.resolvers.get(subschema);
if (resolver) {
try {
Expand Down
11 changes: 11 additions & 0 deletions packages/stitch/src/createDelegationPlanBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
} from 'graphql';
import {
DelegationPlanBuilder,
delegationPlanIdMap,
delegationStageIdMap,
isDelegationDebugging,
MergedTypeInfo,
StitchingInfo,
Subschema,
Expand Down Expand Up @@ -246,6 +249,10 @@ export function createDelegationPlanBuilder(mergedTypeInfo: MergedTypeInfo): Del
);
let { delegationMap } = delegationStage;
while (delegationMap.size) {
if (isDelegationDebugging()) {
const delegationStageId = Math.random().toString(36).slice(2);
delegationStageIdMap.set(delegationMap, delegationStageId);
}
delegationMaps.push(delegationMap);

const { proxiableSubschemas, nonProxiableSubschemas, unproxiableFieldNodes } =
Expand All @@ -261,6 +268,10 @@ export function createDelegationPlanBuilder(mergedTypeInfo: MergedTypeInfo): Del
);
delegationMap = delegationStage.delegationMap;
}
if (isDelegationDebugging()) {
const delegationPlanId = Math.random().toString(36).slice(2);
delegationPlanIdMap.set(delegationMaps, delegationPlanId);
}
return delegationMaps;
});
}
Expand Down
Loading