-
-
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 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
and parentResponsePathAsString
took a ResponsePath
object and did something along the lines of concatenating using .
as a separator. For example,
['foo', 'bar', 'baz']
became foo.bar.baz
.
Where this fell down was that responsePathAsString
wasn't very good with arrays as (as far as I remember) the ResponsePath
object didn't have any concept of an array element's index within the wider collection.
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!
@allyjweir This is great! I'm curious to know what the 2 utils functions would look like.