Last active
May 14, 2020 17:48
-
-
Save allyjweir/ffc2cb36ed814d2be6ca78d0db54a1a2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
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!
It’s less a beeline feature and more graphql-js needs to expose the data.
Good luck!
…On Thu, 14 May 2020 at 18:35, Hugh Boylan ***@***.***> wrote:
***@***.**** commented on this gist.
------------------------------
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!
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<https://gist.github.com/ffc2cb36ed814d2be6ca78d0db54a1a2#gistcomment-3304342>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA3YWBJDPJD2MR4B5CIUVQLRRQTW7ANCNFSM4NAUH2MA>
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.