-
-
Save allyjweir/ffc2cb36ed814d2be6ca78d0db54a1a2 to your computer and use it in GitHub Desktop.
| import { GraphQLRequestContext } from 'apollo-server-core/dist/requestPipelineAPI'; | |
| import { Request } from 'apollo-server-env'; | |
| import beeline from 'honeycomb-beeline'; | |
| import { | |
| DocumentNode, | |
| GraphQLResolveInfo, | |
| ResponsePath, | |
| ExecutionArgs, | |
| GraphQLOutputType, | |
| GraphQLCompositeType, | |
| } from 'graphql'; | |
| import { GraphQLExtension, EndHandler } from 'graphql-extensions'; | |
| import uuid from 'uuid'; | |
| import { responsePathAsString, parentResponsePathAsString } from './utils'; | |
| export default class HoneycombTracingExtension<TContext = any> implements GraphQLExtension<TContext> { | |
| public spans: Map<string, any>; | |
| public queryString; | |
| public documentAST | |
| public operationName; | |
| public constructor() { | |
| this.spans = new Map<string, any>(); | |
| } | |
| public requestDidStart(o: { | |
| request: Request; | |
| queryString?: string; | |
| parsedQuery?: DocumentNode; | |
| variables?: Record<string, any>; | |
| persistedQueryHit?: boolean; | |
| persistedQueryRegister?: boolean; | |
| context: TContext; | |
| extensions?: Record<string, any>; | |
| requestContext: GraphQLRequestContext<TContext>; | |
| }): EndHandler { | |
| // Generally, we'll get queryString here and not parsedQuery; we only get | |
| // parsedQuery if you're using an OperationStore. In normal cases we'll get | |
| // our documentAST in the execution callback after it is parsed. | |
| this.queryString = o.queryString; | |
| this.documentAST = o.parsedQuery; | |
| if (beeline.traceActive()) { | |
| const rootSpan = beeline.startSpan({ name: 'graphql_query' }); | |
| this.spans.set('', rootSpan); | |
| } else { | |
| beeline.startTrace(); | |
| } | |
| return () => { | |
| const rootSpanToFinish = this.spans.get(''); | |
| rootSpanToFinish['graphql.query_string'] = this.queryString; | |
| beeline.finishSpan(rootSpanToFinish); | |
| }; | |
| } | |
| public executionDidStart(o: { executionArgs: ExecutionArgs }) { | |
| // If the operationName is explicitly provided, save it. If there's just one | |
| // named operation, the client doesn't have to provide it, but we still want | |
| // to know the operation name so that the server can identify the query by | |
| // it without having to parse a signature. | |
| // | |
| // Fortunately, in the non-error case, we can just pull this out of | |
| // the first call to willResolveField's `info` argument. In an | |
| // error case (eg, the operationName isn't found, or there are more | |
| // than one operation and no specified operationName) it's OK to continue | |
| // to file this trace under the empty operationName. | |
| if (o.executionArgs.operationName) { | |
| this.operationName = o.executionArgs.operationName; | |
| } | |
| this.documentAST = o.executionArgs.document; | |
| } | |
| public willResolveField( | |
| _source: any, | |
| _args: { [argName: string]: any }, | |
| _context: TContext, | |
| info: GraphQLResolveInfo, | |
| ): ((error: Error | null, result: any) => void) | void { | |
| if (this.operationName === undefined) { | |
| this.operationName = (info.operation.name && info.operation.name.value) || ''; | |
| } | |
| this.newSpan(info.path, info.returnType, info.parentType); | |
| return () => { | |
| const spanToFinish = this.spans.get(responsePathAsString(info.path)); | |
| spanToFinish['graphql.operation_name'] = this.operationName; | |
| if (spanToFinish) { | |
| beeline.finishSpan(spanToFinish); | |
| } | |
| }; | |
| } | |
| private newSpan(path: ResponsePath, returnType: GraphQLOutputType, parentType: GraphQLCompositeType) { | |
| const fieldResponsePath = responsePathAsString(path); | |
| const context = { | |
| name: 'graphql_field_resolver', | |
| 'graphql.type': returnType.toString(), | |
| 'graphql.parent_type': parentType.toString(), | |
| 'graphql.field_path': fieldResponsePath, | |
| }; | |
| const id = path && path.key; | |
| if (path && path.prev && typeof path.prev.key === 'number') { | |
| context['graphql.field_name'] = `${path.prev.key}.${id}`; | |
| } else { | |
| context['graphql.field_name'] = id; | |
| } | |
| let parentSpanId; | |
| if (path && path.prev) { | |
| const parentSpan = this.spans.get(parentResponsePathAsString(path)); | |
| if (parentSpan) { | |
| parentSpanId = parentSpan['trace.span_id']; | |
| } | |
| } | |
| const span = beeline.startSpan(context, uuid(), parentSpanId); | |
| this.spans.set(fieldResponsePath, span); | |
| return span; | |
| } | |
| } |
I've run into the same exact issue with arrays. My solution was to drop indices from the parent path, but the parent-child hierarchy still doesn't resolve correctly. This results in several missing parent spans in some traces.
In case others stumble across this, the following might be a helpful starting point which could be further optimized:
const responsePathAsString = (path: ResponsePath) => {
return responsePathAsArray(path).join('.')
}
const parentResponsePathAsString = (path: ResponsePath) => {
const arr = responsePathAsArray(path)
arr.pop()
// remove trailiing indices
let trailingIndex = -1
arr.forEach((value, index) => {
if (typeof value === 'number') {
trailingIndex = index
}
})
if (trailingIndex > -1) {
arr.splice(trailingIndex, arr.length - trailingIndex)
}
// combine array of resolved fields into one
return arr.join('.')
}Yeah at the time I speculated that Apollo were doing some munging to the data to append array indices to the correct spans to make their tracing solution work.
I did consider this with Honeycomb but you need to be able to view multiple events at once and while beelines-nodejs has a pre-send hook, that is on a per event basis and trying to do weird state tracking across events it was going to be a mega-hacky solution.
EDIT: There's lots of discussion around this topic in the Honeycomb Pollinators Slack. Worth joining if you aren't already a member.
That makes sense. For now, I'll just flatten arrays to avoid a hacky solution. Hopefully beeline will support this use case in the future.
Appreciate you sharing this!
I don't make use of this code anymore, it was a prototype at a previous job so not got tonnes of help to give I'm afraid.
IIRC the
responsePathAsStringandparentResponsePathAsStringtook aResponsePathobject and did something along the lines of concatenating using.as a separator. For example,['foo', 'bar', 'baz']becamefoo.bar.baz.Where this fell down was that
responsePathAsStringwasn't very good with arrays as (as far as I remember) theResponsePathobject didn't have any concept of an array element's index within the wider collection.