-
-
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
responsePathAsString
andparentResponsePathAsString
took aResponsePath
object 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
responsePathAsString
wasn't very good with arrays as (as far as I remember) theResponsePath
object didn't have any concept of an array element's index within the wider collection.