I have implemented a LoggingInterceptor
which should be able to access the final GraphQL-Response with its data- and error properties + the original request body and the authenticated user, which has been added to the request by AuthGuard
before.(EDIT: Partially solved by @jay-mcdoniel: user
and body
are accessible through GqlExecutionContext.create(context).getContext()
)
Indeed the Interceptor just provides one fully resolved GraphQL-Object.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(tap(
(allData) => console.log(allData),
(error)=> console.log(error)));
}
}
This is my Interceptor-Class. It's just calling the RxJS-Operator tap
to log the current values of the observable.
If I run the following GraphQL-Request...
mutation {
login(data: { username: "admin", password: "123456" }) {
id
username
token
}
}
... my server answers correctly with the following response-body:
{
"data": {
"login": {
"id": "6f40be3b-cda9-4e6d-97ce-ced3787e9974",
"username": "admin",
"token": "someToken"
}
}
}
But the content of allData
which get's logged to console by my interceptor is the following:
{
id: '6f40be3b-cda9-4e6d-97ce-ced3787e9974',
isAdmin: true,
username: 'admin',
firstname: null,
lastname: null,
email: null,
created: 2019-07-05T15:11:31.606Z,
token: 'someToken'
}
Instead I would like to see the information of the real response-body.
I have additionally tried to access the HttpResponse by context.switchToHttp().getResponse()
. But this only contains the parameters of the mutation-login-method:
{
data: [Object: null prototype] { username: 'admin', password: '123456' }
}
EDIT:
console.log(GqlExecutionContext.create(context).getContext());
prints (still no GraphQL-ResponseBody):
{
headers: {
/*...*/
},
user: /*...*/,
body: {
operationName: null,
variables: {},
query: 'mutation {\n login(data: {username: "admin", password: ' +
'"123456"}) {\n token\n id\n username\n isAdmin\n }\n' +
'}\n'
},
res: ServerResponse {
_events: [Object: null prototype] { finish: [Function: bound resOnFinish] },
_eventsCount: 1,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
_last: false,
chunkedEncoding: false,
shouldKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: true,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: '',
finished: false,
_headerSent: false,
socket: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
readable: true,
_events: [Object],
_eventsCount: 8,
_maxListeners: undefined,
_writableState: [WritableState],
writable: true,
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: [Server],
_server: [Server],
timeout: 120000,
parser: [HTTPParser],
on: [Function: socketOnWrap],
_paused: false,
_httpMessage: [Circular],
[Symbol(asyncId)]: 566,
[Symbol(kHandle)]: [TCP],
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: Timeout {
/*...*/
},
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
connection: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
readable: true,
_events: [Object],
_eventsCount: 8,
_maxListeners: undefined,
_writableState: [WritableState],
writable: true,
allowHalfOpen: true,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: [Server],
_server: [Server],
timeout: 120000,
parser: [HTTPParser],
on: [Function: socketOnWrap],
_paused: false,
_httpMessage: [Circular],
[Symbol(asyncId)]: 566,
[Symbol(kHandle)]: [TCP],
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: Timeout {
_idleTimeout: 120000,
_idlePrev: [TimersList],
_idleNext: [TimersList],
_idleStart: 3273,
_onTimeout: [Function: bound ],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(refed)]: false,
[Symbol(asyncId)]: 567,
[Symbol(triggerId)]: 566
},
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
_header: null,
_onPendingData: [Function: bound updateOutgoingData],
_sent100: false,
_expect_continue: false,
req: IncomingMessage {
/*...*/
},
locals: [Object: null prototype] {},
[Symbol(isCorked)]: false,
[Symbol(outHeadersKey)]: [Object: null prototype] {
'x-powered-by': [Array],
'access-control-allow-origin': [Array]
}
},
_extensionStack: GraphQLExtensionStack { extensions: [ [CacheControlExtension] ] }
}
The interceptor is actually called before and after the response, or it should be at least, so that you can have pre-request logic (request in) and post-request logic (response out). You should be able to do all pre-request processing before you call next.hanlde()
and then you should be able to use the RxJS Observable operators
such as tap
or map
after a pipe()
call. Your allData
variable should have all the information from the request/response, and you can even use the context
variable for getting even more information.
What does allData
currently print for you? Have you tried GqlExecutionContext.create(context).getContext().req
or GqlExecutionContext.create(context).getContext().res
? These are shown being used in the Guards
documentation to get the request and response objects like you would with a normal HTTP call.
That's how I did that in my Logging Interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private readonly logger: Logger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// default REST Api
if (context.getType() === 'http') {
...
...
}
// Graphql
if (context.getType<GqlContextType>() === 'graphql') {
const gqlContext = GqlExecutionContext.create(context);
const info = gqlContext.getInfo();
const res: Response = gqlContext.getContext().res;
// Get user that sent request
const userId = context.getArgByIndex(2).req.user.userId;
const parentType = info.parentType.name;
const fieldName = info.fieldName;
const body = info.fieldNodes[0]?.loc?.source?.body;
const message = `GraphQL - ${parentType} - ${fieldName}`;
// Add request ID,so it can be tracked with response
const requestId = uuidv4();
// Put to header, so can attach it to response as well
res.set('requestId', requestId);
const trace = {
userId,
body
};
this.logger.info(`requestId: ${requestId}`, {
context: message,
trace
});
return next.handle().pipe(
tap({
next: (val: unknown): void => {
this.logNext(val, context);
}
})
);
}
return next.handle();
}
/**
* Method to log response message
*/
private logNext(body: unknown, context: ExecutionContext): void {
// default REST Api
if (context.getType() === 'http') {
...
...
}
if (context.getType<GqlContextType>() === 'graphql') {
const gqlContext = GqlExecutionContext.create(context);
const info = gqlContext.getInfo();
const parentType = info.parentType.name;
const fieldName = info.fieldName;
const res: Response = gqlContext.getContext().res;
const message = `GraphQL - ${parentType} - ${fieldName}`;
// Remove secure fields from request body and headers
const secureBody = secureReqBody(body);
const requestId = res.getHeader('requestId');
// Log trace message
const trace = {
body: { ...secureBody }
};
this.logger.info(`requestId: ${requestId}`, {
context: message,
trace
});
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With