@graphql-yoga/plugin-apollo-usage-report 0.13.0 → 0.13.1-alpha-20260116132159-d18a95d04a1e11d197fdf672a8be4836ceed0818
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/dist/index.cjs +152 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.mts +125 -0
- package/dist/index.mjs +151 -0
- package/dist/reporter.cjs +118 -0
- package/dist/reporter.d.cts +23 -0
- package/dist/reporter.d.mts +23 -0
- package/dist/reporter.mjs +117 -0
- package/dist/stats.cjs +344 -0
- package/dist/stats.d.cts +123 -0
- package/dist/stats.d.mts +123 -0
- package/dist/stats.mjs +344 -0
- package/package.json +53 -28
- package/LICENSE +0 -23
- package/cjs/index.js +0 -186
- package/cjs/package.json +0 -1
- package/cjs/reporter.js +0 -142
- package/cjs/stats.js +0 -547
- package/esm/index.js +0 -182
- package/esm/reporter.js +0 -136
- package/esm/stats.js +0 -539
- package/typings/index.d.cts +0 -122
- package/typings/index.d.ts +0 -122
- package/typings/reporter.d.cts +0 -21
- package/typings/reporter.d.ts +0 -21
- package/typings/stats.d.cts +0 -136
- package/typings/stats.d.ts +0 -136
package/cjs/stats.js
DELETED
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Copied from https://github.com/apollographql/apollo-server/blob/8c6579e5b61276b62dc7e30e6fac9a4242e24daa/packages/server/src/plugin/usageReporting/stats.ts
|
|
3
|
-
/* eslint-disable */
|
|
4
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.DurationHistogram = exports.OurContextualizedStats = exports.OurReport = exports.SizeEstimator = void 0;
|
|
6
|
-
exports.iterateOverTrace = iterateOverTrace;
|
|
7
|
-
const usage_reporting_protobuf_1 = require("@apollo/usage-reporting-protobuf");
|
|
8
|
-
// protobuf.js exports both a class and an interface (starting with I) for each
|
|
9
|
-
// message type. The class is what it produces when it decodes the message; the
|
|
10
|
-
// interface is what is accepted as input. We build up our messages using custom
|
|
11
|
-
// types implementing the interfaces, so that we can take advantage of the
|
|
12
|
-
// js_use_toArray option we added to our protobuf.js fork which allows us to use
|
|
13
|
-
// classes like DurationHistogram to generate repeated fields. We end up
|
|
14
|
-
// re-creating most of the report structure as custom classes (starting with
|
|
15
|
-
// "Our"). TypeScript validates that we've properly listed all of the message
|
|
16
|
-
// fields with the appropriate types (we use `Required` to ensure we implement
|
|
17
|
-
// all message fields). Using our own classes has other advantages, like being
|
|
18
|
-
// able to specify that nested messages are instances of the same class rather
|
|
19
|
-
// than the interface type and thus that they have non-null fields (because the
|
|
20
|
-
// interface type allows all fields to be optional, even though the protobuf
|
|
21
|
-
// format doesn't differentiate between missing and falsey).
|
|
22
|
-
class SizeEstimator {
|
|
23
|
-
bytes = 0;
|
|
24
|
-
}
|
|
25
|
-
exports.SizeEstimator = SizeEstimator;
|
|
26
|
-
class OurReport {
|
|
27
|
-
header;
|
|
28
|
-
// Apollo Server includes each operation either as aggregated stats or as a
|
|
29
|
-
// trace, but not both. Other reporting agents such as Apollo Router include
|
|
30
|
-
// all operations in stats (even those that are sent as traces), and they set
|
|
31
|
-
// this flag to true.
|
|
32
|
-
tracesPreAggregated = false;
|
|
33
|
-
constructor(header) {
|
|
34
|
-
this.header = header;
|
|
35
|
-
}
|
|
36
|
-
tracesPerQuery = Object.create(null);
|
|
37
|
-
endTime = null;
|
|
38
|
-
operationCount = 0;
|
|
39
|
-
// A rough estimate of the number of bytes currently in the report. We start
|
|
40
|
-
// at zero and don't count `header` and `endTime`, which have the same size
|
|
41
|
-
// for every report. This really is a rough estimate, so we don't stress too
|
|
42
|
-
// much about counting bytes for the tags and string/message lengths, etc:
|
|
43
|
-
// we mostly just count the lengths of strings plus some estimates for the
|
|
44
|
-
// messages with a bunch of numbers in them.
|
|
45
|
-
//
|
|
46
|
-
// We store this in a class so we can pass it down as a reference to other
|
|
47
|
-
// methods which increment it.
|
|
48
|
-
sizeEstimator = new SizeEstimator();
|
|
49
|
-
ensureCountsAreIntegers() {
|
|
50
|
-
for (const tracesAndStats of Object.values(this.tracesPerQuery)) {
|
|
51
|
-
tracesAndStats.ensureCountsAreIntegers();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
addTrace({ statsReportKey, trace, asTrace, referencedFieldsByType,
|
|
55
|
-
// The max size a trace can be before it is sent as stats. Note that the
|
|
56
|
-
// Apollo reporting ingress server will never store any traces over 10mb
|
|
57
|
-
// anyway. They will still be converted to stats as we would do here.
|
|
58
|
-
maxTraceBytes = 10 * 1024 * 1024, nonFtv1ErrorPaths, }) {
|
|
59
|
-
const tracesAndStats = this.getTracesAndStats({
|
|
60
|
-
statsReportKey,
|
|
61
|
-
referencedFieldsByType,
|
|
62
|
-
});
|
|
63
|
-
if (asTrace) {
|
|
64
|
-
const encodedTrace = usage_reporting_protobuf_1.Trace.encode(trace).finish();
|
|
65
|
-
if (!isNaN(maxTraceBytes) && encodedTrace.length > maxTraceBytes) {
|
|
66
|
-
tracesAndStats.statsWithContext.addTrace(trace, this.sizeEstimator, nonFtv1ErrorPaths);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
tracesAndStats.trace.push(encodedTrace);
|
|
70
|
-
this.sizeEstimator.bytes += 2 + encodedTrace.length;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
tracesAndStats.statsWithContext.addTrace(trace, this.sizeEstimator, nonFtv1ErrorPaths);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
getTracesAndStats({ statsReportKey, referencedFieldsByType, }) {
|
|
78
|
-
const existing = this.tracesPerQuery[statsReportKey];
|
|
79
|
-
if (existing) {
|
|
80
|
-
return existing;
|
|
81
|
-
}
|
|
82
|
-
this.sizeEstimator.bytes += estimatedBytesForString(statsReportKey);
|
|
83
|
-
// Update the size estimator for the referenced field structure.
|
|
84
|
-
for (const [typeName, referencedFieldsForType] of Object.entries(referencedFieldsByType)) {
|
|
85
|
-
// Two bytes each for the map entry and for the ReferencedFieldsForType,
|
|
86
|
-
// and for the isInterface bool if it's set.
|
|
87
|
-
this.sizeEstimator.bytes += 2 + 2;
|
|
88
|
-
if (referencedFieldsForType.isInterface) {
|
|
89
|
-
this.sizeEstimator.bytes += 2;
|
|
90
|
-
}
|
|
91
|
-
this.sizeEstimator.bytes += estimatedBytesForString(typeName);
|
|
92
|
-
for (const fieldName of referencedFieldsForType.fieldNames) {
|
|
93
|
-
this.sizeEstimator.bytes += estimatedBytesForString(fieldName);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Include the referenced fields map in the report. (In an ideal world we
|
|
97
|
-
// could have a slightly more sophisticated protocol and ingestion pipeline
|
|
98
|
-
// that allowed us to only have to send this data once for each
|
|
99
|
-
// schema/operation pair.)
|
|
100
|
-
return (this.tracesPerQuery[statsReportKey] = new OurTracesAndStats(referencedFieldsByType));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
exports.OurReport = OurReport;
|
|
104
|
-
class OurTracesAndStats {
|
|
105
|
-
referencedFieldsByType;
|
|
106
|
-
constructor(referencedFieldsByType) {
|
|
107
|
-
this.referencedFieldsByType = referencedFieldsByType;
|
|
108
|
-
}
|
|
109
|
-
trace = [];
|
|
110
|
-
statsWithContext = new StatsByContext();
|
|
111
|
-
internalTracesContributingToStats = [];
|
|
112
|
-
ensureCountsAreIntegers() {
|
|
113
|
-
this.statsWithContext.ensureCountsAreIntegers();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
class StatsByContext {
|
|
117
|
-
map = Object.create(null);
|
|
118
|
-
/**
|
|
119
|
-
* This function is used by the protobuf generator to convert this map into
|
|
120
|
-
* an array of contextualized stats to serialize
|
|
121
|
-
*/
|
|
122
|
-
toArray() {
|
|
123
|
-
return Object.values(this.map);
|
|
124
|
-
}
|
|
125
|
-
ensureCountsAreIntegers() {
|
|
126
|
-
for (const contextualizedStats of Object.values(this.map)) {
|
|
127
|
-
contextualizedStats.ensureCountsAreIntegers();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
addTrace(trace, sizeEstimator, nonFtv1ErrorPaths) {
|
|
131
|
-
this.getContextualizedStats(trace, sizeEstimator).addTrace(trace, sizeEstimator, nonFtv1ErrorPaths);
|
|
132
|
-
}
|
|
133
|
-
getContextualizedStats(trace, sizeEstimator) {
|
|
134
|
-
const statsContext = {
|
|
135
|
-
clientName: trace.clientName,
|
|
136
|
-
clientVersion: trace.clientVersion,
|
|
137
|
-
};
|
|
138
|
-
const statsContextKey = JSON.stringify(statsContext);
|
|
139
|
-
const existing = this.map[statsContextKey];
|
|
140
|
-
if (existing) {
|
|
141
|
-
return existing;
|
|
142
|
-
}
|
|
143
|
-
// Adding a ContextualizedStats means adding a StatsContext plus a
|
|
144
|
-
// QueryLatencyStats. Let's guess about 20 bytes for a QueryLatencyStats;
|
|
145
|
-
// it'll be more if more features are used (like cache, APQ, etc).
|
|
146
|
-
sizeEstimator.bytes +=
|
|
147
|
-
20 + estimatedBytesForString(trace.clientName) + estimatedBytesForString(trace.clientVersion);
|
|
148
|
-
const contextualizedStats = new OurContextualizedStats(statsContext);
|
|
149
|
-
this.map[statsContextKey] = contextualizedStats;
|
|
150
|
-
return contextualizedStats;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
class OurContextualizedStats {
|
|
154
|
-
context;
|
|
155
|
-
queryLatencyStats = new OurQueryLatencyStats();
|
|
156
|
-
perTypeStat = Object.create(null);
|
|
157
|
-
constructor(context) {
|
|
158
|
-
this.context = context;
|
|
159
|
-
}
|
|
160
|
-
ensureCountsAreIntegers() {
|
|
161
|
-
for (const typeStat of Object.values(this.perTypeStat)) {
|
|
162
|
-
typeStat.ensureCountsAreIntegers();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// Extract statistics from the trace, and increment the estimated report size.
|
|
166
|
-
// We only add to the estimate when adding whole sub-messages. If it really
|
|
167
|
-
// mattered, we could do a lot more careful things like incrementing it
|
|
168
|
-
// whenever a numeric field on queryLatencyStats gets incremented over 0.
|
|
169
|
-
addTrace(trace, sizeEstimator, nonFtv1ErrorPaths = []) {
|
|
170
|
-
const { fieldExecutionWeight } = trace;
|
|
171
|
-
if (!fieldExecutionWeight) {
|
|
172
|
-
this.queryLatencyStats.requestsWithoutFieldInstrumentation++;
|
|
173
|
-
}
|
|
174
|
-
this.queryLatencyStats.requestCount++;
|
|
175
|
-
if (trace.fullQueryCacheHit) {
|
|
176
|
-
this.queryLatencyStats.cacheLatencyCount.incrementDuration(trace.durationNs);
|
|
177
|
-
this.queryLatencyStats.cacheHits++;
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
this.queryLatencyStats.latencyCount.incrementDuration(trace.durationNs);
|
|
181
|
-
}
|
|
182
|
-
// We only provide stats about cache TTLs on cache misses (ie, TTLs directly
|
|
183
|
-
// calculated by the backend), not for cache hits. This matches the
|
|
184
|
-
// behavior we've had for a while when converting traces into statistics
|
|
185
|
-
// in Studio's servers.
|
|
186
|
-
if (!trace.fullQueryCacheHit && trace.cachePolicy?.maxAgeNs != null) {
|
|
187
|
-
switch (trace.cachePolicy.scope) {
|
|
188
|
-
case usage_reporting_protobuf_1.Trace.CachePolicy.Scope.PRIVATE:
|
|
189
|
-
this.queryLatencyStats.privateCacheTtlCount.incrementDuration(trace.cachePolicy.maxAgeNs);
|
|
190
|
-
break;
|
|
191
|
-
case usage_reporting_protobuf_1.Trace.CachePolicy.Scope.PUBLIC:
|
|
192
|
-
this.queryLatencyStats.publicCacheTtlCount.incrementDuration(trace.cachePolicy.maxAgeNs);
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (trace.persistedQueryHit) {
|
|
197
|
-
this.queryLatencyStats.persistedQueryHits++;
|
|
198
|
-
}
|
|
199
|
-
if (trace.persistedQueryRegister) {
|
|
200
|
-
this.queryLatencyStats.persistedQueryMisses++;
|
|
201
|
-
}
|
|
202
|
-
if (trace.forbiddenOperation) {
|
|
203
|
-
this.queryLatencyStats.forbiddenOperationCount++;
|
|
204
|
-
}
|
|
205
|
-
if (trace.registeredOperation) {
|
|
206
|
-
this.queryLatencyStats.registeredOperationCount++;
|
|
207
|
-
}
|
|
208
|
-
let hasError = false;
|
|
209
|
-
const errorPathStats = new Set();
|
|
210
|
-
const traceNodeStats = (node, path) => {
|
|
211
|
-
// Generate error stats and error path information
|
|
212
|
-
if (node.error?.length) {
|
|
213
|
-
hasError = true;
|
|
214
|
-
let currPathErrorStats = this.queryLatencyStats.rootErrorStats;
|
|
215
|
-
path.toArray().forEach(subPath => {
|
|
216
|
-
currPathErrorStats = currPathErrorStats.getChild(subPath, sizeEstimator);
|
|
217
|
-
});
|
|
218
|
-
errorPathStats.add(currPathErrorStats);
|
|
219
|
-
currPathErrorStats.errorsCount += node.error.length;
|
|
220
|
-
}
|
|
221
|
-
if (fieldExecutionWeight) {
|
|
222
|
-
// The actual field name behind the node; originalFieldName is set
|
|
223
|
-
// if an alias was used, otherwise responseName. (This is falsey for
|
|
224
|
-
// nodes that are not fields (root, array index, etc).)
|
|
225
|
-
const fieldName = node.originalFieldName || node.responseName;
|
|
226
|
-
// Protobuf doesn't really differentiate between "unset" and "falsey" so
|
|
227
|
-
// we're mostly actually checking that these things are non-empty string /
|
|
228
|
-
// non-zero numbers. The time fields represent the number of nanoseconds
|
|
229
|
-
// since the beginning of the entire trace, so let's pretend for the
|
|
230
|
-
// moment that it's plausible for a node to start or even end exactly when
|
|
231
|
-
// the trace started (ie, for the time values to be 0). This is unlikely
|
|
232
|
-
// in practice (everything should take at least 1ns). In practice we only
|
|
233
|
-
// write `type` and `parentType` on a Node when we write `startTime`, so
|
|
234
|
-
// the main thing we're looking out for by checking the time values is
|
|
235
|
-
// whether we somehow failed to write `endTime` at the end of the field;
|
|
236
|
-
// in this case, the `endTime >= startTime` check won't match.
|
|
237
|
-
if (node.parentType &&
|
|
238
|
-
fieldName &&
|
|
239
|
-
node.type &&
|
|
240
|
-
node.endTime != null &&
|
|
241
|
-
node.startTime != null &&
|
|
242
|
-
node.endTime >= node.startTime) {
|
|
243
|
-
const typeStat = this.getTypeStat(node.parentType, sizeEstimator);
|
|
244
|
-
const fieldStat = typeStat.getFieldStat(fieldName, node.type, sizeEstimator);
|
|
245
|
-
fieldStat.errorsCount += node.error?.length ?? 0;
|
|
246
|
-
fieldStat.observedExecutionCount++;
|
|
247
|
-
fieldStat.estimatedExecutionCount += fieldExecutionWeight;
|
|
248
|
-
// Note: this is actually counting the number of resolver calls for this
|
|
249
|
-
// field that had at least one error, not the number of overall GraphQL
|
|
250
|
-
// queries that had at least one error for this field. That doesn't seem
|
|
251
|
-
// to match the name, but it does match the other implementations of this
|
|
252
|
-
// logic.
|
|
253
|
-
fieldStat.requestsWithErrorsCount += (node.error?.length ?? 0) > 0 ? 1 : 0;
|
|
254
|
-
fieldStat.latencyCount.incrementDuration(node.endTime - node.startTime,
|
|
255
|
-
// The latency histogram is always "estimated"; we don't track
|
|
256
|
-
// "observed" and "estimated" separately.
|
|
257
|
-
fieldExecutionWeight);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return false;
|
|
261
|
-
};
|
|
262
|
-
iterateOverTrace(trace, traceNodeStats, true);
|
|
263
|
-
// iterate over nonFtv1ErrorPaths, using some bits from traceNodeStats function
|
|
264
|
-
for (const { subgraph, path } of nonFtv1ErrorPaths) {
|
|
265
|
-
hasError = true;
|
|
266
|
-
if (path) {
|
|
267
|
-
let currPathErrorStats = this.queryLatencyStats.rootErrorStats.getChild(`service:${subgraph}`, sizeEstimator);
|
|
268
|
-
path.forEach(subPath => {
|
|
269
|
-
if (typeof subPath === 'string') {
|
|
270
|
-
currPathErrorStats = currPathErrorStats.getChild(subPath, sizeEstimator);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
errorPathStats.add(currPathErrorStats);
|
|
274
|
-
currPathErrorStats.errorsCount += 1;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
for (const errorPath of errorPathStats) {
|
|
278
|
-
errorPath.requestsWithErrorsCount += 1;
|
|
279
|
-
}
|
|
280
|
-
if (hasError) {
|
|
281
|
-
this.queryLatencyStats.requestsWithErrorsCount++;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
getTypeStat(parentType, sizeEstimator) {
|
|
285
|
-
const existing = this.perTypeStat[parentType];
|
|
286
|
-
if (existing) {
|
|
287
|
-
return existing;
|
|
288
|
-
}
|
|
289
|
-
sizeEstimator.bytes += estimatedBytesForString(parentType);
|
|
290
|
-
const typeStat = new OurTypeStat();
|
|
291
|
-
this.perTypeStat[parentType] = typeStat;
|
|
292
|
-
return typeStat;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
exports.OurContextualizedStats = OurContextualizedStats;
|
|
296
|
-
class OurQueryLatencyStats {
|
|
297
|
-
latencyCount = new DurationHistogram();
|
|
298
|
-
requestCount = 0;
|
|
299
|
-
requestsWithoutFieldInstrumentation = 0;
|
|
300
|
-
cacheHits = 0;
|
|
301
|
-
persistedQueryHits = 0;
|
|
302
|
-
persistedQueryMisses = 0;
|
|
303
|
-
cacheLatencyCount = new DurationHistogram();
|
|
304
|
-
rootErrorStats = new OurPathErrorStats();
|
|
305
|
-
requestsWithErrorsCount = 0;
|
|
306
|
-
publicCacheTtlCount = new DurationHistogram();
|
|
307
|
-
privateCacheTtlCount = new DurationHistogram();
|
|
308
|
-
registeredOperationCount = 0;
|
|
309
|
-
forbiddenOperationCount = 0;
|
|
310
|
-
}
|
|
311
|
-
class OurPathErrorStats {
|
|
312
|
-
children = Object.create(null);
|
|
313
|
-
errorsCount = 0;
|
|
314
|
-
requestsWithErrorsCount = 0;
|
|
315
|
-
getChild(subPath, sizeEstimator) {
|
|
316
|
-
const existing = this.children[subPath];
|
|
317
|
-
if (existing) {
|
|
318
|
-
return existing;
|
|
319
|
-
}
|
|
320
|
-
const child = new OurPathErrorStats();
|
|
321
|
-
this.children[subPath] = child;
|
|
322
|
-
// Include a few bytes in the estimate for the numbers etc.
|
|
323
|
-
sizeEstimator.bytes += estimatedBytesForString(subPath) + 4;
|
|
324
|
-
return child;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
class OurTypeStat {
|
|
328
|
-
perFieldStat = Object.create(null);
|
|
329
|
-
getFieldStat(fieldName, returnType, sizeEstimator) {
|
|
330
|
-
const existing = this.perFieldStat[fieldName];
|
|
331
|
-
if (existing) {
|
|
332
|
-
return existing;
|
|
333
|
-
}
|
|
334
|
-
// Rough estimate of 10 bytes for the numbers in the FieldStat.
|
|
335
|
-
sizeEstimator.bytes +=
|
|
336
|
-
estimatedBytesForString(fieldName) + estimatedBytesForString(returnType) + 10;
|
|
337
|
-
const fieldStat = new OurFieldStat(returnType);
|
|
338
|
-
this.perFieldStat[fieldName] = fieldStat;
|
|
339
|
-
return fieldStat;
|
|
340
|
-
}
|
|
341
|
-
ensureCountsAreIntegers() {
|
|
342
|
-
for (const fieldStat of Object.values(this.perFieldStat)) {
|
|
343
|
-
fieldStat.ensureCountsAreIntegers();
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
class OurFieldStat {
|
|
348
|
-
returnType;
|
|
349
|
-
errorsCount = 0;
|
|
350
|
-
observedExecutionCount = 0;
|
|
351
|
-
// Note that this number isn't necessarily an integer while it is being
|
|
352
|
-
// aggregated. Before encoding as a protobuf we call ensureCountsAreIntegers
|
|
353
|
-
// which floors it.
|
|
354
|
-
estimatedExecutionCount = 0;
|
|
355
|
-
requestsWithErrorsCount = 0;
|
|
356
|
-
latencyCount = new DurationHistogram();
|
|
357
|
-
constructor(returnType) {
|
|
358
|
-
this.returnType = returnType;
|
|
359
|
-
}
|
|
360
|
-
ensureCountsAreIntegers() {
|
|
361
|
-
// This is the only one that ever can receive non-integers.
|
|
362
|
-
this.estimatedExecutionCount = Math.floor(this.estimatedExecutionCount);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
function estimatedBytesForString(s) {
|
|
366
|
-
// 2 is for the tag (field ID + wire type) plus the encoded length. (The
|
|
367
|
-
// encoded length takes up more than 1 byte for strings that are longer than
|
|
368
|
-
// 127 bytes, but this is an estimate.)
|
|
369
|
-
return 2 + Buffer.byteLength(s);
|
|
370
|
-
}
|
|
371
|
-
class DurationHistogram {
|
|
372
|
-
// Note that it's legal for the values in "buckets" to be non-integers; they
|
|
373
|
-
// will be floored by toArray (which is called by the protobuf encoder).
|
|
374
|
-
// (We take advantage of this for field latencies specifically, because
|
|
375
|
-
// the ability to return a non-1 weight from fieldLevelInstrumentation
|
|
376
|
-
// means we want to build up our histograms as floating-point rather than
|
|
377
|
-
// rounding after every operation.)
|
|
378
|
-
buckets;
|
|
379
|
-
static BUCKET_COUNT = 384;
|
|
380
|
-
static EXPONENT_LOG = Math.log(1.1);
|
|
381
|
-
toArray() {
|
|
382
|
-
let bufferedZeroes = 0;
|
|
383
|
-
const outputArray = [];
|
|
384
|
-
for (const value of this.buckets) {
|
|
385
|
-
if (value === 0) {
|
|
386
|
-
bufferedZeroes++;
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
if (bufferedZeroes === 1) {
|
|
390
|
-
outputArray.push(0);
|
|
391
|
-
}
|
|
392
|
-
else if (bufferedZeroes !== 0) {
|
|
393
|
-
outputArray.push(-bufferedZeroes);
|
|
394
|
-
}
|
|
395
|
-
outputArray.push(Math.floor(value));
|
|
396
|
-
bufferedZeroes = 0;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return outputArray;
|
|
400
|
-
}
|
|
401
|
-
static durationToBucket(durationNs) {
|
|
402
|
-
const log = Math.log(durationNs / 1000.0);
|
|
403
|
-
const unboundedBucket = Math.ceil(log / DurationHistogram.EXPONENT_LOG);
|
|
404
|
-
// Compare <= 0 to catch -0 and -infinity
|
|
405
|
-
return unboundedBucket <= 0 || Number.isNaN(unboundedBucket)
|
|
406
|
-
? 0
|
|
407
|
-
: unboundedBucket >= DurationHistogram.BUCKET_COUNT
|
|
408
|
-
? DurationHistogram.BUCKET_COUNT - 1
|
|
409
|
-
: unboundedBucket;
|
|
410
|
-
}
|
|
411
|
-
incrementDuration(durationNs, value = 1) {
|
|
412
|
-
this.incrementBucket(DurationHistogram.durationToBucket(durationNs), value);
|
|
413
|
-
return this;
|
|
414
|
-
}
|
|
415
|
-
incrementBucket(bucket, value = 1) {
|
|
416
|
-
if (bucket >= DurationHistogram.BUCKET_COUNT) {
|
|
417
|
-
// Since we don't have fixed size arrays I'd rather throw the error manually
|
|
418
|
-
throw Error('Bucket is out of bounds of the buckets array');
|
|
419
|
-
}
|
|
420
|
-
// Extend the array if we haven't gotten it long enough to handle the new bucket
|
|
421
|
-
if (bucket >= this.buckets.length) {
|
|
422
|
-
const oldLength = this.buckets.length;
|
|
423
|
-
this.buckets.length = bucket + 1;
|
|
424
|
-
this.buckets.fill(0, oldLength);
|
|
425
|
-
}
|
|
426
|
-
this.buckets[bucket] += value; // ! is safe, we've already ensured the array is long enough
|
|
427
|
-
}
|
|
428
|
-
combine(otherHistogram) {
|
|
429
|
-
for (let i = 0; i < otherHistogram.buckets.length; i++) {
|
|
430
|
-
this.incrementBucket(i, otherHistogram.buckets[i]);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
constructor(options) {
|
|
434
|
-
const initSize = options?.initSize || 74;
|
|
435
|
-
const buckets = options?.buckets;
|
|
436
|
-
const arrayInitSize = Math.max(buckets?.length || 0, initSize);
|
|
437
|
-
this.buckets = Array(arrayInitSize).fill(0);
|
|
438
|
-
if (buckets) {
|
|
439
|
-
buckets.forEach((val, index) => (this.buckets[index] = val));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
exports.DurationHistogram = DurationHistogram;
|
|
444
|
-
/**
|
|
445
|
-
* Iterates over the entire trace, calling `f` on each Trace.Node found. It
|
|
446
|
-
* looks under the "root" node as well as any inside the query plan. If any `f`
|
|
447
|
-
* returns true, it stops walking the tree.
|
|
448
|
-
*
|
|
449
|
-
* Each call to `f` will receive an object that implements ResponseNamePath. If
|
|
450
|
-
* `includePath` is true, `f` can call `toArray()` on it to convert the
|
|
451
|
-
* linked-list representation to an array of the response name (field name)
|
|
452
|
-
* nodes that you navigate to get to the node (including a "service:subgraph"
|
|
453
|
-
* top-level node if this is a federated trace). Note that we don't add anything
|
|
454
|
-
* to the path for index (list element) nodes. This is because the only use case
|
|
455
|
-
* we have (error path statistics) does not care about list indexes (it's not
|
|
456
|
-
* that interesting to know that sometimes an error was at foo.3.bar and
|
|
457
|
-
* sometimes foo.5.bar, vs just generally foo.bar).
|
|
458
|
-
*
|
|
459
|
-
* If `includePath` is false, we don't bother to build up the linked lists, and
|
|
460
|
-
* calling `toArray()` will throw.
|
|
461
|
-
*/
|
|
462
|
-
function iterateOverTrace(trace, f, includePath) {
|
|
463
|
-
const rootPath = includePath
|
|
464
|
-
? new RootCollectingPathsResponseNamePath()
|
|
465
|
-
: notCollectingPathsResponseNamePath;
|
|
466
|
-
if (trace.root) {
|
|
467
|
-
if (iterateOverTraceNode(trace.root, rootPath, f))
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
if (trace.queryPlan) {
|
|
471
|
-
if (iterateOverQueryPlan(trace.queryPlan, rootPath, f))
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Helper for iterateOverTrace; returns true to stop the overall walk.
|
|
476
|
-
function iterateOverQueryPlan(node, rootPath, f) {
|
|
477
|
-
if (!node)
|
|
478
|
-
return false;
|
|
479
|
-
if (node.fetch?.trace?.root && node.fetch.serviceName) {
|
|
480
|
-
return iterateOverTraceNode(node.fetch.trace.root, rootPath.child(`service:${node.fetch.serviceName}`), f);
|
|
481
|
-
}
|
|
482
|
-
if (node.flatten?.node) {
|
|
483
|
-
return iterateOverQueryPlan(node.flatten.node, rootPath, f);
|
|
484
|
-
}
|
|
485
|
-
if (node.parallel?.nodes) {
|
|
486
|
-
// We want to stop as soon as some call returns true, which happens to be
|
|
487
|
-
// exactly what 'some' does.
|
|
488
|
-
return node.parallel.nodes.some(node => iterateOverQueryPlan(node, rootPath, f));
|
|
489
|
-
}
|
|
490
|
-
if (node.sequence?.nodes) {
|
|
491
|
-
// We want to stop as soon as some call returns true, which happens to be
|
|
492
|
-
// exactly what 'some' does.
|
|
493
|
-
return node.sequence.nodes.some(node => iterateOverQueryPlan(node, rootPath, f));
|
|
494
|
-
}
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
|
-
// Helper for iterateOverTrace; returns true to stop the overall walk.
|
|
498
|
-
function iterateOverTraceNode(node, path, f) {
|
|
499
|
-
// Invoke the function; if it returns true, don't descend and tell callers to
|
|
500
|
-
// stop walking.
|
|
501
|
-
if (f(node, path)) {
|
|
502
|
-
return true;
|
|
503
|
-
}
|
|
504
|
-
return (
|
|
505
|
-
// We want to stop as soon as some call returns true, which happens to be
|
|
506
|
-
// exactly what 'some' does.
|
|
507
|
-
node.child?.some(child => {
|
|
508
|
-
const childPath = child.responseName ? path.child(child.responseName) : path;
|
|
509
|
-
return iterateOverTraceNode(child, childPath, f);
|
|
510
|
-
}) ?? false);
|
|
511
|
-
}
|
|
512
|
-
const notCollectingPathsResponseNamePath = {
|
|
513
|
-
toArray() {
|
|
514
|
-
throw Error('not collecting paths!');
|
|
515
|
-
},
|
|
516
|
-
child() {
|
|
517
|
-
return this;
|
|
518
|
-
},
|
|
519
|
-
};
|
|
520
|
-
class RootCollectingPathsResponseNamePath {
|
|
521
|
-
toArray() {
|
|
522
|
-
return [];
|
|
523
|
-
}
|
|
524
|
-
child(responseName) {
|
|
525
|
-
return new ChildCollectingPathsResponseNamePath(responseName, this);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
class ChildCollectingPathsResponseNamePath {
|
|
529
|
-
responseName;
|
|
530
|
-
prev;
|
|
531
|
-
constructor(responseName, prev) {
|
|
532
|
-
this.responseName = responseName;
|
|
533
|
-
this.prev = prev;
|
|
534
|
-
}
|
|
535
|
-
toArray() {
|
|
536
|
-
const out = [];
|
|
537
|
-
let curr = this;
|
|
538
|
-
while (curr instanceof ChildCollectingPathsResponseNamePath) {
|
|
539
|
-
out.push(curr.responseName);
|
|
540
|
-
curr = curr.prev;
|
|
541
|
-
}
|
|
542
|
-
return out.reverse();
|
|
543
|
-
}
|
|
544
|
-
child(responseName) {
|
|
545
|
-
return new ChildCollectingPathsResponseNamePath(responseName, this);
|
|
546
|
-
}
|
|
547
|
-
}
|