@graphql-yoga/plugin-apollo-usage-report
Version:
Apollo's GraphOS usage report plugin for GraphQL Yoga.
344 lines (342 loc) • 13.6 kB
JavaScript
let _apollo_usage_reporting_protobuf = require("@apollo/usage-reporting-protobuf");
//#region src/stats.ts
var SizeEstimator = class {
bytes = 0;
};
var OurReport = class {
tracesPreAggregated = false;
constructor(header) {
this.header = header;
}
tracesPerQuery = Object.create(null);
endTime = null;
operationCount = 0;
sizeEstimator = new SizeEstimator();
ensureCountsAreIntegers() {
for (const tracesAndStats of Object.values(this.tracesPerQuery)) tracesAndStats.ensureCountsAreIntegers();
}
addTrace({ statsReportKey, trace, asTrace, referencedFieldsByType, maxTraceBytes = 10 * 1024 * 1024, nonFtv1ErrorPaths }) {
const tracesAndStats = this.getTracesAndStats({
statsReportKey,
referencedFieldsByType
});
if (asTrace) {
const encodedTrace = _apollo_usage_reporting_protobuf.Trace.encode(trace).finish();
if (!isNaN(maxTraceBytes) && encodedTrace.length > maxTraceBytes) tracesAndStats.statsWithContext.addTrace(trace, this.sizeEstimator, nonFtv1ErrorPaths);
else {
tracesAndStats.trace.push(encodedTrace);
this.sizeEstimator.bytes += 2 + encodedTrace.length;
}
} else tracesAndStats.statsWithContext.addTrace(trace, this.sizeEstimator, nonFtv1ErrorPaths);
}
getTracesAndStats({ statsReportKey, referencedFieldsByType }) {
const existing = this.tracesPerQuery[statsReportKey];
if (existing) return existing;
this.sizeEstimator.bytes += estimatedBytesForString(statsReportKey);
for (const [typeName, referencedFieldsForType] of Object.entries(referencedFieldsByType)) {
this.sizeEstimator.bytes += 4;
if (referencedFieldsForType.isInterface) this.sizeEstimator.bytes += 2;
this.sizeEstimator.bytes += estimatedBytesForString(typeName);
for (const fieldName of referencedFieldsForType.fieldNames) this.sizeEstimator.bytes += estimatedBytesForString(fieldName);
}
return this.tracesPerQuery[statsReportKey] = new OurTracesAndStats(referencedFieldsByType);
}
};
var OurTracesAndStats = class {
constructor(referencedFieldsByType) {
this.referencedFieldsByType = referencedFieldsByType;
}
trace = [];
statsWithContext = new StatsByContext();
internalTracesContributingToStats = [];
ensureCountsAreIntegers() {
this.statsWithContext.ensureCountsAreIntegers();
}
};
var StatsByContext = class {
map = Object.create(null);
/**
* This function is used by the protobuf generator to convert this map into
* an array of contextualized stats to serialize
*/
toArray() {
return Object.values(this.map);
}
ensureCountsAreIntegers() {
for (const contextualizedStats of Object.values(this.map)) contextualizedStats.ensureCountsAreIntegers();
}
addTrace(trace, sizeEstimator, nonFtv1ErrorPaths) {
this.getContextualizedStats(trace, sizeEstimator).addTrace(trace, sizeEstimator, nonFtv1ErrorPaths);
}
getContextualizedStats(trace, sizeEstimator) {
const statsContext = {
clientName: trace.clientName,
clientVersion: trace.clientVersion
};
const statsContextKey = JSON.stringify(statsContext);
const existing = this.map[statsContextKey];
if (existing) return existing;
sizeEstimator.bytes += 20 + estimatedBytesForString(trace.clientName) + estimatedBytesForString(trace.clientVersion);
const contextualizedStats = new OurContextualizedStats(statsContext);
this.map[statsContextKey] = contextualizedStats;
return contextualizedStats;
}
};
var OurContextualizedStats = class {
queryLatencyStats = new OurQueryLatencyStats();
perTypeStat = Object.create(null);
constructor(context) {
this.context = context;
}
ensureCountsAreIntegers() {
for (const typeStat of Object.values(this.perTypeStat)) typeStat.ensureCountsAreIntegers();
}
addTrace(trace, sizeEstimator, nonFtv1ErrorPaths = []) {
const { fieldExecutionWeight } = trace;
if (!fieldExecutionWeight) this.queryLatencyStats.requestsWithoutFieldInstrumentation++;
this.queryLatencyStats.requestCount++;
if (trace.fullQueryCacheHit) {
this.queryLatencyStats.cacheLatencyCount.incrementDuration(trace.durationNs);
this.queryLatencyStats.cacheHits++;
} else this.queryLatencyStats.latencyCount.incrementDuration(trace.durationNs);
if (!trace.fullQueryCacheHit && trace.cachePolicy?.maxAgeNs != null) switch (trace.cachePolicy.scope) {
case _apollo_usage_reporting_protobuf.Trace.CachePolicy.Scope.PRIVATE:
this.queryLatencyStats.privateCacheTtlCount.incrementDuration(trace.cachePolicy.maxAgeNs);
break;
case _apollo_usage_reporting_protobuf.Trace.CachePolicy.Scope.PUBLIC:
this.queryLatencyStats.publicCacheTtlCount.incrementDuration(trace.cachePolicy.maxAgeNs);
break;
}
if (trace.persistedQueryHit) this.queryLatencyStats.persistedQueryHits++;
if (trace.persistedQueryRegister) this.queryLatencyStats.persistedQueryMisses++;
if (trace.forbiddenOperation) this.queryLatencyStats.forbiddenOperationCount++;
if (trace.registeredOperation) this.queryLatencyStats.registeredOperationCount++;
let hasError = false;
const errorPathStats = /* @__PURE__ */ new Set();
const traceNodeStats = (node, path) => {
if (node.error?.length) {
hasError = true;
let currPathErrorStats = this.queryLatencyStats.rootErrorStats;
path.toArray().forEach((subPath) => {
currPathErrorStats = currPathErrorStats.getChild(subPath, sizeEstimator);
});
errorPathStats.add(currPathErrorStats);
currPathErrorStats.errorsCount += node.error.length;
}
if (fieldExecutionWeight) {
const fieldName = node.originalFieldName || node.responseName;
if (node.parentType && fieldName && node.type && node.endTime != null && node.startTime != null && node.endTime >= node.startTime) {
const fieldStat = this.getTypeStat(node.parentType, sizeEstimator).getFieldStat(fieldName, node.type, sizeEstimator);
fieldStat.errorsCount += node.error?.length ?? 0;
fieldStat.observedExecutionCount++;
fieldStat.estimatedExecutionCount += fieldExecutionWeight;
fieldStat.requestsWithErrorsCount += (node.error?.length ?? 0) > 0 ? 1 : 0;
fieldStat.latencyCount.incrementDuration(node.endTime - node.startTime, fieldExecutionWeight);
}
}
return false;
};
iterateOverTrace(trace, traceNodeStats, true);
for (const { subgraph, path } of nonFtv1ErrorPaths) {
hasError = true;
if (path) {
let currPathErrorStats = this.queryLatencyStats.rootErrorStats.getChild(`service:${subgraph}`, sizeEstimator);
path.forEach((subPath) => {
if (typeof subPath === "string") currPathErrorStats = currPathErrorStats.getChild(subPath, sizeEstimator);
});
errorPathStats.add(currPathErrorStats);
currPathErrorStats.errorsCount += 1;
}
}
for (const errorPath of errorPathStats) errorPath.requestsWithErrorsCount += 1;
if (hasError) this.queryLatencyStats.requestsWithErrorsCount++;
}
getTypeStat(parentType, sizeEstimator) {
const existing = this.perTypeStat[parentType];
if (existing) return existing;
sizeEstimator.bytes += estimatedBytesForString(parentType);
const typeStat = new OurTypeStat();
this.perTypeStat[parentType] = typeStat;
return typeStat;
}
};
var OurQueryLatencyStats = class {
latencyCount = new DurationHistogram();
requestCount = 0;
requestsWithoutFieldInstrumentation = 0;
cacheHits = 0;
persistedQueryHits = 0;
persistedQueryMisses = 0;
cacheLatencyCount = new DurationHistogram();
rootErrorStats = new OurPathErrorStats();
requestsWithErrorsCount = 0;
publicCacheTtlCount = new DurationHistogram();
privateCacheTtlCount = new DurationHistogram();
registeredOperationCount = 0;
forbiddenOperationCount = 0;
};
var OurPathErrorStats = class OurPathErrorStats {
children = Object.create(null);
errorsCount = 0;
requestsWithErrorsCount = 0;
getChild(subPath, sizeEstimator) {
const existing = this.children[subPath];
if (existing) return existing;
const child = new OurPathErrorStats();
this.children[subPath] = child;
sizeEstimator.bytes += estimatedBytesForString(subPath) + 4;
return child;
}
};
var OurTypeStat = class {
perFieldStat = Object.create(null);
getFieldStat(fieldName, returnType, sizeEstimator) {
const existing = this.perFieldStat[fieldName];
if (existing) return existing;
sizeEstimator.bytes += estimatedBytesForString(fieldName) + estimatedBytesForString(returnType) + 10;
const fieldStat = new OurFieldStat(returnType);
this.perFieldStat[fieldName] = fieldStat;
return fieldStat;
}
ensureCountsAreIntegers() {
for (const fieldStat of Object.values(this.perFieldStat)) fieldStat.ensureCountsAreIntegers();
}
};
var OurFieldStat = class {
errorsCount = 0;
observedExecutionCount = 0;
estimatedExecutionCount = 0;
requestsWithErrorsCount = 0;
latencyCount = new DurationHistogram();
constructor(returnType) {
this.returnType = returnType;
}
ensureCountsAreIntegers() {
this.estimatedExecutionCount = Math.floor(this.estimatedExecutionCount);
}
};
function estimatedBytesForString(s) {
return 2 + Buffer.byteLength(s);
}
var DurationHistogram = class DurationHistogram {
buckets;
static BUCKET_COUNT = 384;
static EXPONENT_LOG = Math.log(1.1);
toArray() {
let bufferedZeroes = 0;
const outputArray = [];
for (const value of this.buckets) if (value === 0) bufferedZeroes++;
else {
if (bufferedZeroes === 1) outputArray.push(0);
else if (bufferedZeroes !== 0) outputArray.push(-bufferedZeroes);
outputArray.push(Math.floor(value));
bufferedZeroes = 0;
}
return outputArray;
}
static durationToBucket(durationNs) {
const log = Math.log(durationNs / 1e3);
const unboundedBucket = Math.ceil(log / DurationHistogram.EXPONENT_LOG);
return unboundedBucket <= 0 || Number.isNaN(unboundedBucket) ? 0 : unboundedBucket >= DurationHistogram.BUCKET_COUNT ? DurationHistogram.BUCKET_COUNT - 1 : unboundedBucket;
}
incrementDuration(durationNs, value = 1) {
this.incrementBucket(DurationHistogram.durationToBucket(durationNs), value);
return this;
}
incrementBucket(bucket, value = 1) {
if (bucket >= DurationHistogram.BUCKET_COUNT) throw Error("Bucket is out of bounds of the buckets array");
if (bucket >= this.buckets.length) {
const oldLength = this.buckets.length;
this.buckets.length = bucket + 1;
this.buckets.fill(0, oldLength);
}
this.buckets[bucket] += value;
}
combine(otherHistogram) {
for (let i = 0; i < otherHistogram.buckets.length; i++) this.incrementBucket(i, otherHistogram.buckets[i]);
}
constructor(options) {
const initSize = options?.initSize || 74;
const buckets = options?.buckets;
const arrayInitSize = Math.max(buckets?.length || 0, initSize);
this.buckets = Array(arrayInitSize).fill(0);
if (buckets) buckets.forEach((val, index) => this.buckets[index] = val);
}
};
/**
* Iterates over the entire trace, calling `f` on each Trace.Node found. It
* looks under the "root" node as well as any inside the query plan. If any `f`
* returns true, it stops walking the tree.
*
* Each call to `f` will receive an object that implements ResponseNamePath. If
* `includePath` is true, `f` can call `toArray()` on it to convert the
* linked-list representation to an array of the response name (field name)
* nodes that you navigate to get to the node (including a "service:subgraph"
* top-level node if this is a federated trace). Note that we don't add anything
* to the path for index (list element) nodes. This is because the only use case
* we have (error path statistics) does not care about list indexes (it's not
* that interesting to know that sometimes an error was at foo.3.bar and
* sometimes foo.5.bar, vs just generally foo.bar).
*
* If `includePath` is false, we don't bother to build up the linked lists, and
* calling `toArray()` will throw.
*/
function iterateOverTrace(trace, f, includePath) {
const rootPath = includePath ? new RootCollectingPathsResponseNamePath() : notCollectingPathsResponseNamePath;
if (trace.root) {
if (iterateOverTraceNode(trace.root, rootPath, f)) return;
}
if (trace.queryPlan) {
if (iterateOverQueryPlan(trace.queryPlan, rootPath, f)) return;
}
}
function iterateOverQueryPlan(node, rootPath, f) {
if (!node) return false;
if (node.fetch?.trace?.root && node.fetch.serviceName) return iterateOverTraceNode(node.fetch.trace.root, rootPath.child(`service:${node.fetch.serviceName}`), f);
if (node.flatten?.node) return iterateOverQueryPlan(node.flatten.node, rootPath, f);
if (node.parallel?.nodes) return node.parallel.nodes.some((node) => iterateOverQueryPlan(node, rootPath, f));
if (node.sequence?.nodes) return node.sequence.nodes.some((node) => iterateOverQueryPlan(node, rootPath, f));
return false;
}
function iterateOverTraceNode(node, path, f) {
if (f(node, path)) return true;
return node.child?.some((child) => {
return iterateOverTraceNode(child, child.responseName ? path.child(child.responseName) : path, f);
}) ?? false;
}
const notCollectingPathsResponseNamePath = {
toArray() {
throw Error("not collecting paths!");
},
child() {
return this;
}
};
var RootCollectingPathsResponseNamePath = class {
toArray() {
return [];
}
child(responseName) {
return new ChildCollectingPathsResponseNamePath(responseName, this);
}
};
var ChildCollectingPathsResponseNamePath = class ChildCollectingPathsResponseNamePath {
constructor(responseName, prev) {
this.responseName = responseName;
this.prev = prev;
}
toArray() {
const out = [];
let curr = this;
while (curr instanceof ChildCollectingPathsResponseNamePath) {
out.push(curr.responseName);
curr = curr.prev;
}
return out.reverse();
}
child(responseName) {
return new ChildCollectingPathsResponseNamePath(responseName, this);
}
};
//#endregion
exports.OurReport = OurReport;