UNPKG

@apollo/server

Version:
75 lines (65 loc) 2.95 kB
import { LRUCache } from 'lru-cache'; import type { Trace } from '@apollo/usage-reporting-protobuf'; import { iterateOverTrace } from './iterateOverTrace.js'; import { DurationHistogram } from './durationHistogram.js'; export function defaultSendOperationsAsTrace() { // We keep an LRU cache mapping from a trace key (which consists of the // operation as defined by statsReportKey, the rough duration of the // operation, what minute the operation ended at, etc) to `true` if we've seen // it recently. We actually split this into one cache per minute so we can // throw away a full minute's worth of cache at once; we keep only the last // three minutes. // Note that if a trace is over a certain size, we will always send it as // stats. We check this within the addTrace function of the OurReport class so // that we don't have to encode these large traces twice. const cache = new LRUCache<string, true>({ // 3MiB limit, very much approximately since we can't be sure how V8 might // be storing these strings internally. Though this should be enough to // store a fair amount of trace keys. // A future version of this might expose some // configuration option to grow the cache, but ideally, we could do that // dynamically based on the resources available to the server, and not add // more configuration surface area. Hopefully the warning message will allow // us to evaluate the need with more validated input from those that receive // it. maxSize: Math.pow(2, 20), sizeCalculation: (_val, key) => { return (key && Buffer.byteLength(key)) || 0; }, }); return (trace: Trace, statsReportKey: string): boolean => { const endTimeSeconds = trace.endTime?.seconds; if (endTimeSeconds == null) { throw Error('programming error: endTime not set on trace'); } const hasErrors = traceHasErrors(trace); const cacheKey = JSON.stringify([ statsReportKey, DurationHistogram.durationToBucket(trace.durationNs), // What minute it started at Math.floor(endTimeSeconds / 60), // If the trace has an error, send one errored trace per 5 second interval // instead of the normal minutely bucket a non-errored trace takes. hasErrors ? Math.floor(endTimeSeconds / 5) : '', ]); // If we've already seen something roughly like this, don't send as a trace. if (cache.get(cacheKey)) { return false; } cache.set(cacheKey, true); return true; }; } // Returns true if any node on the trace has errors. (If this ends up being a // hot spot, we can precalculate it in TraceTreeBuilder.) function traceHasErrors(trace: Trace): boolean { let hasErrors = false; function traceNodeStats(node: Trace.INode): boolean { if ((node.error?.length ?? 0) > 0) { hasErrors = true; } return hasErrors; } iterateOverTrace(trace, traceNodeStats, false); return hasErrors; }