UNPKG

@graphql-yoga/plugin-apollo-usage-report

Version:

Apollo's GraphOS usage report plugin for GraphQL Yoga.

344 lines (342 loc) • 13.6 kB
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;