Hey all,
Now that Prisma just released their excellent blog post about GraphQL Middleware (created by @matic), thought I’d share with the community how I’m logging all queries and mutations, as well as any errors. Errors are logged in a different model, but each error log has a one-to-one relationship to its corresponding session log.
datamodel.graphql
type LogError @model {
# Core
id: ID! @unique
createdAt: DateTime!
updatedAt: DateTime!
# Fields
args: Json
error: String!
LogSession: LogSession! @relation(name: "LogErrorSession", onDelete: SET_NULL)
}
type LogSession @model {
# Core
id: ID! @unique
createdAt: DateTime!
updatedAt: DateTime!
# Fields
args: Json
ipAddress: String
LogError: LogError @relation(name: "LogErrorSession", onDelete: CASCADE)
origin: String!
resolver: String!
}
index.js (using Prisma)
// LOG SESSION MIDDLEWARE
const logSession = async (resolve, parent, args, ctx, info) => {
const session = await ctx.db.mutation.createLogSession({
data: {
args,
// Any custom header info can be captured using
// `ctx.request.headers['__CUSTOM_HEADER_INFO__']`
ipAddress: ctx.request.headers['IPAddress'],
// Might be good practice to log the `origin`, and if not available,
// use `referer`
origin: ctx.request.headers.origin
? ctx.request.headers.origin
: ctx.request.headers.referer,
// The name of the resolver can be captured using `info.fieldName`
resolver: info.fieldName
}
}, `{ id }`)
// Important: notice how we’re adding the session ID as a new arg.
// Later it gets passed into the error logging middleware
const argsWithSession = { ...args, sessionId: session.id }
return await resolve(parent, argsWithSession, ctx, info)
}
// Apply session logger to all query and mutation resolvers individually. I found
// this works cleaner than the approach shared in the Prisma blog post whereby
// you apply the middleware across all resolvers; you can try out that approach
// by replacing `sessionMiddleware` with `logSession` in the `middlewares` option
// below.
const queryResolvers = Object.keys(resolvers.Query).reduce((result, item) => {
result[item] = logSession
return result
}, {})
const mutationResolvers = Object.keys(resolvers.Mutation).reduce((result, item) => {
result[item] = logSession
return result
}, {})
// Add session logger to middleware
const sessionMiddleware = {
Query: {
...queryResolvers
},
Mutation: {
...mutationResolvers
}
}
// LOG ERROR MIDDLEWARE
const errorMiddleware = async (resolve, root, args, context, info) => {
// First resolve all resolvers...
try {
return await resolve(root, args, context, info)
}
// ...and if an error occurs in any of them, log it and tie it to session.
// It works for both controlled and uncontrolled errors:
// - controlled errors can be initiated via:
// throw new Error('This is an error message.')
// - uncontrolled errors occur if there’s an unintentional error in the code
catch (err) {
// Notice how the sessionId from the `logSession` arg is now being used
// to connect the error node to its corresponding session node
const sessionId = args.sessionId
// We want to delete the sessionId from the args since we just need
// the sessionId to connect the error node to its session node.
delete args.sessionId
context.db.mutation.createLogError({
data: {
// Args is the object that contains all of the arguments that were
// used in calling the resolver; collecting the args may help us
// understand what went wrong.
args,
// Error message
error: err,
// Connect the error node to its session node
LogSession: {
connect: {
id: sessionId
}
}
}
})
// Throw error to communicate error to browser.
throw new Error(err)
}
}
// SERVER
const server = new GraphQLServer({
typeDefs: 'src/schema.graphql',
resolvers,
// Add middleware here. NOTE: the sequence is important:
// first the session logging middleware, then the error logging middleware
middlewares: [sessionMiddleware, errorMiddleware],
context: req => {
return {
...req,
db: new Prisma({
typeDefs: 'src/generated/prisma.graphql',
endpoint: process.env.PRISMA_ENDPOINT,
secret: process.env.PRISMA_SECRET,
debug: true
})
}
}
})