| 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 | 1x
1x
1x
1x
1x
42x
7x
1x
6x
6x
6x
6x
6x
6x
5x
5x
1x
5x
5x
1x
1x
1x
16x
16x
16x
16x
16x
16x
33x
33x
33x
33x
33x
16x
16x
16x
6x
5x
16x
9x
16x
16x
16x
16x
1x
1x
15x
15x
2x
2x
13x
2x
2x
11x
10x
1x
1x
8x
8x
1x
10x
1x
1x
1x
2x
1x
1x
1x
6x
6x
6x
| import now from 'performance-now';
import uuid from 'node-uuid';
import request from 'request';
import { forEachField } from 'graphql-tools';
const TRACER_INGRESS_URL = 'https://nim-test-ingress.appspot.com';
class Tracer {
// TODO make sure Tracer can NEVER crash the server.
// maybe wrap everything in try/catch, but need to test that.
constructor({ TRACER_APP_KEY, sendReports = true, reportFilterFn, proxy }) {
if (!TRACER_APP_KEY || TRACER_APP_KEY.length < 36) {
throw new Error('Tracer requires a well-formatted TRACER_APP_KEY');
}
// TODO check that sendReports is a boolean
// TODO check that report filter fn is a function (if defined)
this.TRACER_APP_KEY = TRACER_APP_KEY;
this.startTime = (new Date()).getTime();
this.startHrTime = now();
this.sendReports = sendReports;
this.reportFilterFn = reportFilterFn;
this.proxy = proxy;
}
sendReport(report) {
let filteredEvents = report.events;
if (this.reportFilterFn) {
filteredEvents = report.events.filter(this.reportFilterFn);
}
const options = {
url: TRACER_INGRESS_URL,
proxy: this.proxy,
method: 'PUT',
headers: {
'user-agent': `apollo tracer v${report.tracerApiVersion}`,
},
json: {
...report,
events: filteredEvents,
},
};
request(options, (err) => {
Eif (err) {
console.error('Error trying to report to tracer backend:', err.message);
return;
}
// console.log('status', response.statusCode);
});
}
newLoggerInstance() {
const queryId = uuid.v4();
const events = [];
let idCounter = 0;
const startTime = (new Date()).getTime();
const startHrTime = now();
const log = (type, data = null) => {
const id = idCounter++;
const timestamp = now();
// const timestamp = (new Date()).getTime();
// console.log(timestamp, type, id, data);
events.push({ id, timestamp, type, data });
return id;
};
const report = () => {
return {
TRACER_APP_KEY: this.TRACER_APP_KEY,
tracerApiVersion: '0.1.0',
queryId,
startTime,
startHrTime,
events,
};
};
const submit = () => {
if (this.sendReports) {
this.sendReport(report());
}
};
return {
log,
report,
submit,
};
}
/* log(type, data = null) {
// TODO ensure props is a valid props thingy
// TODO ensure info is a valid info thingy
// TODO ensure type is a valid type thingy
const id = this.idCounter++;
const timestamp = now();
// const timestamp = (new Date()).getTime();
console.log(timestamp, type, id, data);
this.events.push({ id, timestamp, type, data });
return id;
}
report() {
return {
queryId: this.queryId,
startTime: this.startTime,
startHrTime: this.startHrTime,
events: this.events,
};
} */
}
function decorateWithTracer(fn, info) {
return (p, a, ctx, i) => {
const startEventId = ctx.tracer.log('resolver.start', info);
let result;
try {
result = fn(p, a, ctx, i);
} catch (e) {
// console.log('yeah, it errored directly');
ctx.tracer.log('resolver.end', {
...info,
resolverError: {
message: e.message,
stack: e.stack,
},
startEventId,
});
throw e;
}
try {
if (result === null) {
ctx.tracer.log('resolver.end', { ...info, returnedNull: true, startEventId });
return result;
}
if (typeof result === 'undefined') {
ctx.tracer.log('resolver.end', { ...info, returnedUndefined: true, startEventId });
return result;
}
if (typeof result.then === 'function') {
result.then((res) => {
ctx.tracer.log('resolver.end', { ...info, startEventId });
return res;
})
.catch((err) => {
// console.log('whoa, it threw an error!');
ctx.tracer.log('resolver.end', { ...info, startEventId });
throw err;
});
} else {
// console.log('did not return a promise. logging now');
ctx.tracer.log('resolver.end', { ...info, startEventId });
}
return result;
} catch (e) {
// XXX this should basically never happen
// if it does happen, we want to be able to collect these events.
ctx.tracer.log('tracer.error', {
...info,
result,
tracerError: {
message: e.message,
stack: e.stack,
},
startEventId,
});
ctx.tracer.log('resolver.end', { ...info, startEventId });
return result;
}
};
}
// This function modifies the schema in place to add tracing around all resolve functions
function addTracingToResolvers(schema) {
// XXX this is a hacky way of making sure that the schema only gets decorated
// with tracer once.
if (schema._apolloTracerApplied) {
// console.log('Tracing already added to resolve functions. Not adding again.');
return;
}
// eslint-disable-next-line no-param-reassign
schema._apolloTracerApplied = true;
forEachField(schema, (field, typeName, fieldName) => {
const functionName = `${typeName}.${fieldName}`;
Eif (field.resolve) {
// eslint-disable-next-line no-param-reassign
field.resolve = decorateWithTracer(
field.resolve,
{ type: 'resolve', functionName },
);
}
});
}
export { Tracer, decorateWithTracer, addTracingToResolvers };
|