GraphQL middleware for session and error logging


#1

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
      })
    }
  }
})

#2

This is so cool! Have you ever thought of creating a library out of it?


#3

That’s a fantastic idea, @matic! Never done this before… since you’re the expert, how would you feel if we did this together? I could learn from you that way. Would that work?


#4

Let’s do this! Are you already in our Slack group?


#5

This is cool :ok_hand:

I’m also using something similar. Provide a package and set this up as a simple SaaS model :stuck_out_tongue: