Skip to content

Commit 16ff694

Browse files
authored
docs(instrumentation): Add documentation for new Instrumentation API (#6572)
1 parent 7d80892 commit 16ff694

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed

packages/web/docs/src/content/gateway/other-features/custom-plugins.mdx

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,218 @@ Prefer `onRequestParse` when possible, or wrap the hook code in a `try` block.
826826
| `serverContext` | The final context object that is shared between all hooks and the GraphQL execution. [Learn more about the context](https://the-guild.dev/graphql/yoga-server/docs/features/context#server-context). |
827827
| `response` | The outgoing HTTP response as WHATWG `Response` object. [Learn more about the response interface](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
828828

829+
### `instrumentation`
830+
831+
An optional `instrumentation` instance can be present in the plugin.
832+
833+
This `Instrumentation` instance allows to wrap an entire phase execution (including all plugin
834+
hooks), meaning running code just before, just after and around the execution of the phase.
835+
836+
It is an advanced feature that most plugins should not need. Its main usage is for tracing or
837+
monitoring purposes.
838+
839+
Instrumentation doesn't have access to input/output of a phase, use hooks to have access to those
840+
data. If needed, we recommend to share data between instrumentation and hooks with a `WeakMap` and
841+
the given `context` as the key.
842+
843+
All instrumentation takes 2 parameters:
844+
845+
- `payload`: an object containing the graphql `context`, http `request`, or the subgraph
846+
`executionRequest` depending on the instrument.
847+
- `wrapped`: The function representing the execution of the phase. It takes no parameters, and
848+
returns `void` (or `Promise<void>` for asynchrone phases). **This function must always be
849+
called**. If this function returns a `Promise`, the instrument should return a `Promise` resolving
850+
after it.
851+
852+
#### Example
853+
854+
```ts
855+
const useMyTracer = () => ({
856+
instrumentation: {
857+
async request({ request }, wrapped) {
858+
const start = performance.now()
859+
// The `wrapped` function represent the execution of this phase.
860+
// Its argument or result are not accessible, it can only be used to perform action before and after it, or modify its execution context.
861+
await wrapped()
862+
const end = performance.now()
863+
console.log('request', request.headers['Trace-ID'], 'processed in', end - start)
864+
},
865+
async operation({ context }, wrapped) {
866+
const start = performance.now()
867+
await wrapped()
868+
const end = performance.now()
869+
console.log('operation', context.params.operationName, 'executed in', end - start)
870+
}
871+
}
872+
})
873+
```
874+
875+
An more extensive example usage of this API can be found in
876+
[`useOpenTelemetry` plugin](https://github.com/graphql-hive/gateway/blob/main/packages/plugins/opentelemetry/src/plugin.ts).
877+
878+
#### Sharing data between Instrumentation and Hooks
879+
880+
Some plugin will require to share some data between instruments and hooks. Since this API doesn't
881+
allow any access to the wrapped phase, you will need to use an indirect way to share data.
882+
883+
The most straightforward solution is to use a `WeakMap` with either the `context`, `request` or
884+
`executionRequest` objects as keys.
885+
886+
```ts
887+
const useMyTracer = () => {
888+
const spans = new WeakMap<unknown, Span>()
889+
return {
890+
instrumentation: {
891+
async operation({ context }, wrapped) {
892+
const span = new Span()
893+
spans.set(context, span)
894+
await wrapped()
895+
span.end()
896+
}
897+
},
898+
onExecute({ args }) {
899+
const span = spans.get(args.contextValue)
900+
span.setAttribute('graphql.operationName', args.operationName)
901+
}
902+
}
903+
}
904+
```
905+
906+
When your plugin grows in complexity, the number of WeakMaps to track data can become difficult to
907+
reason about and maintain. In this case, you use the `withState` utility function.
908+
909+
```ts
910+
import { withState, type GatewayPlugin } from '@graphql-hive/gateway'
911+
912+
type State = { span: Span }
913+
914+
const useMyTracer = () =>
915+
withState<GatewayPlugin, State, State, State>({
916+
instrumentation: {
917+
async operation({ state }, wrapped) {
918+
state.forOperation.span = new Span()
919+
await wrapped()
920+
span.end()
921+
}
922+
},
923+
onExecute({ args, state }) {
924+
state.forOperation.span.setAttribute('graphql.operationName', args.operationName)
925+
}
926+
})
927+
```
928+
929+
#### Instrumentation composition
930+
931+
If multiple plugins have `instrumentation`, they are composed in the same order they are defined the
932+
plugin array (the first is outtermost call, the last is inner most call).
933+
934+
It is possible to customize this composition if it doesn't suite your need (ie. you need hooks and
935+
instrumentation to have a different oreder of execution).
936+
937+
```ts
938+
import { composeInstrumentation, envelop } from '@envelop/core'
939+
940+
const { instrumentation: instrumentation1, ...plugin1 } = usePlugin1()
941+
const { instrumentation: instrumentation2, ...plugin2 } = usePlugin2()
942+
943+
const instrumentation = composeInstrumentation([instrumentation2, instrumentation1])
944+
945+
const getEnveloped = envelop({
946+
plugin: [{ insturments }, plugin1, plugin2]
947+
})
948+
```
949+
950+
#### `request`
951+
952+
Wraps the HTTP request handling. This includes all the plugins `onRequest` and `onResponse` hooks.
953+
954+
This instrument can be asynchronous, the wrapped funcion **can be** asynchronous. Be sure to return
955+
a `Promise` if `wrapped()` returned a `Promise`.
956+
957+
#### `requestParse`
958+
959+
Wraps the parsing of the request phase to extract grapqhl params. This include all the plugins
960+
`onRequestParse` hooks.
961+
962+
This insturment can be asynchronous, the wrapped function **can be** asynchrounous. Be sure to
963+
return a `Promise` if `wrapped()` returns a `Promise`.
964+
965+
#### `operation`
966+
967+
Wraps the Graphql operation execution pipeline. This is called for each graphql operation, meaning
968+
it can be called mutliple time for the same HTTP request if batching is enabled.
969+
970+
This instrument can be asynchronous, the wrapped function **can be** asynchronous. Be sur to return
971+
a `Promise` if `wrapped()` returnd a `Promise`.
972+
973+
#### `init`
974+
975+
Wraps the envelop (the call to `envelop` function) initialisation.
976+
977+
This includes all the plugins `onEnveloped` hooks, and the creation of the Envelop Orchestrator.
978+
979+
This instrumentation must be synchrone, the wrapped function is always synchrone.
980+
981+
#### `parse`
982+
983+
Wraps the parse phase. This includes all the plugins `onParse` hooks and the actual engine `parse`.
984+
985+
This instrument must be synchrone, the wrapped function is always synchrone.
986+
987+
#### `validate`
988+
989+
Wraps the validate phase. This includes all the plugins `onValidate` hooks and the actual engine
990+
`validate`.
991+
992+
This instrument must be synchrone, the wrapped function is always synchrone.
993+
994+
#### `context`
995+
996+
Wraps the context building phase. This includes all the plugins `onContextBuilding` hooks.
997+
998+
This instrument must be synchrone, the wrapped function is always synchrone.
999+
1000+
#### `execute`
1001+
1002+
Wraps the execute phase. This includes all the plugins `onExecute` hooks.
1003+
1004+
This instrument can be asynchrone, the wrapped function **can be** asynchrone. Be sure to `await` or
1005+
use `.then` on the result of the `wrapped` function to run code after the `execute` phase.
1006+
1007+
Note that `wrapped` is not guaranted to return a promise.
1008+
1009+
#### `subscribe`
1010+
1011+
Wraps the subscribe phase. This includes all the plugins `onSubsribe` hooks. Note that it doesn't
1012+
wrap the entire lifetime of the subscription, but only it's intialisation.
1013+
1014+
This instrument can be asynchrone, the wrapped function **can be** asynchrone. Be sure to `await` or
1015+
use `.then` on the result of the `wrapped` function to run code after the `subsribe` phase.
1016+
1017+
Note that `wrapped` is not guaranted to return a promise.
1018+
1019+
#### `subgraphExecute`
1020+
1021+
Wraps the subgraph execution phase. This includes all the plugins `onSubgraphExecute` hooks.
1022+
1023+
This instrumentation can be asynchrone, the wrapped function **can be** asynchrone. Be sure to
1024+
return a `Promise` if `wrapped()`returns a `Promise`.
1025+
1026+
#### `fetch`
1027+
1028+
Wraps the fetch phase. This incudes all the plugins `onFetch` hooks. Note that this can also be
1029+
called outside of the context of a graphql operation, for background tasks like loading the schema.
1030+
1031+
This instrumentation can be asynchrone, the wrapped function **can be** asynchrone. Be sure to
1032+
return a `Promise` if `wrapped()` returns a `Promise`.
1033+
1034+
#### `resultProcess`
1035+
1036+
Wraps the context result processing phase. This includes all the plugins `onResultProcess` hooks.
1037+
1038+
This instrumentation can be asynchrone, the wrapped function **can be** asynchronous. Be sure to
1039+
return a `Promise` if `wrapped()` returns a `Promise`.
1040+
8291041
### Plugin Context
8301042

8311043
Hive Gateway comes with ready-to-use `logger`, `fetch`, cache storage and etc that are shared across

0 commit comments

Comments
 (0)