Skip to content

Commit abf51ce

Browse files
committed
Add support for loggerFn
Allows graphql to log whenever the executor enters or leaves a subtree; an error occurs; or a resolver starts/ends execution.
1 parent b33d1a1 commit abf51ce

File tree

6 files changed

+173
-12
lines changed

6 files changed

+173
-12
lines changed

src/__tests__/starWarsQuery-test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { expect } from 'chai';
1111
import { describe, it } from 'mocha';
1212
import { StarWarsSchema } from './starWarsSchema.js';
1313
import { graphql } from '../graphql';
14+
import { TAG } from '../execution';
1415
import {
1516
GraphQLObjectType,
1617
GraphQLNonNull,
@@ -528,4 +529,59 @@ describe('Star Wars Query Tests', () => {
528529
[ [ 'nullableA', 'nullableA', 'nonNullA', 'nonNullA', 'throws' ] ]);
529530
});
530531
});
532+
533+
describe('Allows to pass in a logger function', () => {
534+
it('Logs on errors', async () => {
535+
const query = `
536+
query HeroNameQuery {
537+
mainHero: hero {
538+
name
539+
story: secretBackstory
540+
}
541+
}
542+
`;
543+
544+
const expected = {
545+
mainHero: {
546+
name: 'R2-D2',
547+
story: null,
548+
}
549+
};
550+
551+
const logged = [];
552+
const expectedLogged = [
553+
[ TAG.RESOLVER_START, [ 'mainHero' ] ],
554+
[ TAG.RESOLVER_END, [ 'mainHero' ] ],
555+
[ TAG.SUBTREE_START, [ 'mainHero' ] ],
556+
[ TAG.RESOLVER_START, [ 'mainHero', 'name' ] ],
557+
[ TAG.RESOLVER_END, [ 'mainHero', 'name' ] ],
558+
[ TAG.SUBTREE_START, [ 'mainHero', 'name' ] ],
559+
[ TAG.SUBTREE_END, [ 'mainHero', 'name' ] ],
560+
[ TAG.RESOLVER_START, [ 'mainHero', 'story' ] ],
561+
[ TAG.RESOLVER_ERROR, [ 'mainHero', 'story' ] ],
562+
[ TAG.RESOLVER_END, [ 'mainHero', 'story' ] ],
563+
[ TAG.SUBTREE_START, [ 'mainHero', 'story' ] ],
564+
[ TAG.SUBTREE_ERROR, [ 'mainHero', 'story' ] ],
565+
[ TAG.SUBTREE_END, [ 'mainHero', 'story' ] ],
566+
[ TAG.SUBTREE_END, [ 'mainHero' ] ],
567+
];
568+
569+
const result = await graphql(
570+
StarWarsSchema,
571+
query,
572+
null,
573+
null,
574+
null,
575+
null,
576+
(tag, payload) => {
577+
const path =
578+
tag === TAG.SUBTREE_ERROR || tag === TAG.RESOLVER_ERROR ?
579+
payload.executionPath : payload;
580+
logged.push([ tag, path ]);
581+
});
582+
583+
expect(result.data).to.deep.equal(expected);
584+
expect(logged).to.deep.equal(expectedLogged);
585+
});
586+
});
531587
});

src/execution/execute.js

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import isNullish from '../jsutils/isNullish';
1515
import { typeFromAST } from '../utilities/typeFromAST';
1616
import { Kind } from '../language';
1717
import { getVariableValues, getArgumentValues } from './values';
18+
import { TAG, passThroughResultAndLog } from './logging';
1819
import {
1920
GraphQLScalarType,
2021
GraphQLObjectType,
@@ -114,7 +115,8 @@ export function execute(
114115
rootValue?: mixed,
115116
contextValue?: mixed,
116117
variableValues?: ?{[key: string]: mixed},
117-
operationName?: ?string
118+
operationName?: ?string,
119+
logFn?: (tag: string, payload: mixed, info: mixed) => void
118120
): Promise<ExecutionResult> {
119121
invariant(schema, 'Must provide schema');
120122
invariant(
@@ -131,7 +133,8 @@ export function execute(
131133
rootValue,
132134
contextValue,
133135
variableValues,
134-
operationName
136+
operationName,
137+
logFn
135138
);
136139

137140
// Return a Promise that will eventually resolve to the data described by
@@ -169,7 +172,8 @@ function buildExecutionContext(
169172
rootValue: mixed,
170173
contextValue: mixed,
171174
rawVariableValues: ?{[key: string]: mixed},
172-
operationName: ?string
175+
operationName: ?string,
176+
logFn?: (tag: string, payload: mixed, info: mixed) => void
173177
): ExecutionContext {
174178
const errors: Array<GraphQLError> = [];
175179
let operation: ?OperationDefinition;
@@ -219,7 +223,8 @@ function buildExecutionContext(
219223
operation,
220224
variableValues,
221225
executionPath,
222-
errors
226+
errors,
227+
logFn
223228
};
224229
}
225230

@@ -586,9 +591,32 @@ function resolveField(
586591
path: exePath
587592
};
588593

594+
const logFn = exeContext.logFn || (() => {});
595+
logFn(TAG.RESOLVER_START, exePath, info);
596+
589597
// Get the resolve function, regardless of if its result is normal
590598
// or abrupt (error).
591-
const result = resolveOrError(resolveFn, source, args, context, info);
599+
let result = resolveOrError(resolveFn, source, args, context, info);
600+
601+
if (isThenable(result)) {
602+
result = ((result: any): Promise).then(
603+
passThroughResultAndLog(logFn, TAG.RESOLVER_END, exePath, info),
604+
reason => {
605+
logFn(TAG.RESOLVER_ERROR, {
606+
error: reason,
607+
executionPath: exePath,
608+
}, info);
609+
return Promise.reject(reason);
610+
});
611+
} else {
612+
if (result instanceof Error) {
613+
logFn(TAG.RESOLVER_ERROR, {
614+
error: result,
615+
executionPath: exePath,
616+
}, info);
617+
}
618+
logFn(TAG.RESOLVER_END, exePath, info);
619+
}
592620

593621
const completed = completeValueCatchingError(
594622
exeContext,
@@ -630,11 +658,30 @@ function completeValueCatchingError(
630658
exePath: Array<string | number>,
631659
result: mixed
632660
): mixed {
661+
const logFn = exeContext.logFn || (() => {});
662+
logFn(TAG.SUBTREE_START, exePath, info);
663+
633664
// If the field type is non-nullable, then it is resolved without any
634665
// protection from errors.
635666
if (returnType instanceof GraphQLNonNull) {
636-
return completeValue(
667+
const completed = completeValue(
637668
exeContext, returnType, fieldASTs, info, exePath, result);
669+
670+
if (isThenable(completed)) {
671+
return ((completed: any): Promise).then(
672+
passThroughResultAndLog(logFn, TAG.SUBTREE_END, exePath, info),
673+
reason => {
674+
logFn(TAG.SUBTREE_ERROR, {
675+
error: reason,
676+
executionPath: exePath
677+
}, info);
678+
logFn(TAG.SUBTREE_END, exePath, info);
679+
return Promise.reject(reason);
680+
});
681+
}
682+
683+
logFn(TAG.SUBTREE_END, exePath, info);
684+
return completed;
638685
}
639686

640687
// Otherwise, error protection is applied, logging the error and resolving
@@ -653,16 +700,34 @@ function completeValueCatchingError(
653700
// error and resolve to null.
654701
// Note: we don't rely on a `catch` method, but we do expect "thenable"
655702
// to take a second callback for the error case.
656-
return ((completed: any): Promise).then(undefined, error => {
657-
exeContext.errors.push(error);
658-
return Promise.resolve(null);
659-
});
703+
return ((completed: any): Promise).then(
704+
passThroughResultAndLog(logFn, TAG.SUBTREE_END, exePath, info),
705+
error => {
706+
exeContext.errors.push(error);
707+
logFn(TAG.SUBTREE_ERROR, {
708+
error,
709+
executionPath: exePath
710+
}, info);
711+
logFn(TAG.SUBTREE_END, exePath, info);
712+
713+
return Promise.resolve(null);
714+
});
660715
}
716+
717+
logFn(TAG.SUBTREE_END, exePath, info);
718+
661719
return completed;
662720
} catch (error) {
663721
// If `completeValue` returned abruptly (threw an error), log the error
664722
// and return null.
665723
exeContext.errors.push(error);
724+
725+
logFn(TAG.SUBTREE_ERROR, {
726+
error,
727+
executionPath: exePath
728+
}, info);
729+
logFn(TAG.SUBTREE_END, exePath, info);
730+
666731
return null;
667732
}
668733
}

src/execution/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
*/
99

1010
export { execute } from './execute';
11+
export { TAG } from './logging';

src/execution/logging.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
export const TAG = {
12+
SUBTREE_START: 'SUBTREE_START',
13+
SUBTREE_END: 'SUBTREE_END',
14+
SUBTREE_ERROR: 'SUBTREE_ERROR',
15+
RESOLVER_START: 'RESOLVER_START',
16+
RESOLVER_END: 'RESOLVER_END',
17+
RESOLVER_ERROR: 'RESOLVER_ERROR',
18+
};
19+
20+
export type GraphQLLoggingTagType = $Keys<typeof TAG>; // eslint-disable-line
21+
22+
export function passThroughResultAndLog(
23+
logFn: any,
24+
tag: GraphQLLoggingTagType,
25+
payload: any,
26+
info: any
27+
): any {
28+
return function (result) {
29+
logFn(tag, payload, info);
30+
return result;
31+
};
32+
}

src/graphql.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,19 @@ import type { GraphQLSchema } from './type/schema';
3939
* The name of the operation to use if requestString contains multiple
4040
* possible operations. Can be omitted if requestString contains only
4141
* one operation.
42+
* logFn:
43+
* The function that is called with a tag of an event, an event payload
44+
* and other execution information.
45+
* The examples include: errors thrown, before and after the resolver call.
4246
*/
4347
export function graphql(
4448
schema: GraphQLSchema,
4549
requestString: string,
4650
rootValue?: mixed,
4751
contextValue?: mixed,
4852
variableValues?: ?{[key: string]: mixed},
49-
operationName?: ?string
53+
operationName?: ?string,
54+
logFn?: (tag: string, payload: mixed, info: mixed) => void
5055
): Promise<GraphQLResult> {
5156
return new Promise(resolve => {
5257
const source = new Source(requestString || '', 'GraphQL request');
@@ -62,7 +67,8 @@ export function graphql(
6267
rootValue,
6368
contextValue,
6469
variableValues,
65-
operationName
70+
operationName,
71+
logFn
6672
)
6773
);
6874
}

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export {
128128
// Execute GraphQL queries.
129129
export {
130130
execute,
131+
TAG
131132
} from './execution';
132133

133134

0 commit comments

Comments
 (0)