-
Notifications
You must be signed in to change notification settings - Fork 2k
/
index.ts
150 lines (134 loc) 路 5.07 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import {
ResponsePath,
responsePathAsArray,
GraphQLType,
} from 'graphql';
import { ApolloServerPlugin } from "apollo-server-plugin-base";
const { PACKAGE_NAME } = require("../package.json").name;
export interface TracingFormat {
version: 1;
startTime: string;
endTime: string;
duration: number;
execution: {
resolvers: {
path: (string | number)[];
parentType: string;
fieldName: string;
returnType: string;
startOffset: number;
duration: number;
}[];
};
}
interface ResolverCall {
path: ResponsePath;
fieldName: string;
parentType: GraphQLType;
returnType: GraphQLType;
startOffset: HighResolutionTime;
endOffset?: HighResolutionTime;
}
export const plugin = (_futureOptions = {}) => (): ApolloServerPlugin => ({
requestDidStart() {
let startWallTime: Date | undefined;
let endWallTime: Date | undefined;
let startHrTime: HighResolutionTime | undefined;
let duration: HighResolutionTime | undefined;
const resolverCalls: ResolverCall[] = [];
startWallTime = new Date();
startHrTime = process.hrtime();
return {
executionDidStart: () => ({
// It's a little odd that we record the end time after execution rather
// than at the end of the whole request, but because we need to include
// our formatted trace in the request itself, we have to record it
// before the request is over!
// Historically speaking: It's WAS odd that we don't do traces for parse
// or validation errors. Reason being: at the time that this was written
// (now a plugin but originally an extension)). That was the case
// because runQuery DIDN'T (again, at the time, when it was an
// extension) support that since format() was only invoked after
// execution.
executionDidEnd: () => {
duration = process.hrtime(startHrTime);
endWallTime = new Date();
},
willResolveField({ info }) {
const resolverCall: ResolverCall = {
path: info.path,
fieldName: info.fieldName,
parentType: info.parentType,
returnType: info.returnType,
startOffset: process.hrtime(startHrTime),
};
resolverCalls.push(resolverCall);
return () => {
resolverCall.endOffset = process.hrtime(startHrTime);
};
},
}),
willSendResponse({ response }) {
// In the event that we are called prior to the initialization of
// critical date metrics, we'll return undefined to signal that the
// extension did not format properly. Any undefined extension
// results are simply purged by the graphql-extensions module.
if (
typeof startWallTime === 'undefined' ||
typeof endWallTime === 'undefined' ||
typeof duration === 'undefined'
) {
return;
}
const extensions =
response.extensions || (response.extensions = Object.create(null));
// Be defensive and make sure nothing else (other plugin, etc.) has
// already used the `tracing` property on `extensions`.
if (typeof extensions.tracing !== 'undefined') {
throw new Error(PACKAGE_NAME + ": Could not add `tracing` to " +
"`extensions` since `tracing` was unexpectedly already present.");
}
// Set the extensions.
extensions.tracing = {
version: 1,
startTime: startWallTime.toISOString(),
endTime: endWallTime.toISOString(),
duration: durationHrTimeToNanos(duration),
execution: {
resolvers: resolverCalls.map(resolverCall => {
const startOffset = durationHrTimeToNanos(
resolverCall.startOffset,
);
const duration = resolverCall.endOffset
? durationHrTimeToNanos(resolverCall.endOffset) - startOffset
: 0;
return {
path: [...responsePathAsArray(resolverCall.path)],
parentType: resolverCall.parentType.toString(),
fieldName: resolverCall.fieldName,
returnType: resolverCall.returnType.toString(),
startOffset,
duration,
};
}),
},
};
},
};
},
})
type HighResolutionTime = [number, number];
// Converts an hrtime array (as returned from process.hrtime) to nanoseconds.
//
// ONLY CALL THIS ON VALUES REPRESENTING DELTAS, NOT ON THE RAW RETURN VALUE
// FROM process.hrtime() WITH NO ARGUMENTS.
//
// The entire point of the hrtime data structure is that the JavaScript Number
// type can't represent all int64 values without loss of precision:
// Number.MAX_SAFE_INTEGER nanoseconds is about 104 days. Calling this function
// on a duration that represents a value less than 104 days is fine. Calling
// this function on an absolute time (which is generally roughly time since
// system boot) is not a good idea.
function durationHrTimeToNanos(hrtime: HighResolutionTime) {
return hrtime[0] * 1e9 + hrtime[1];
}