Skip to content

Middleware to record request and response sizes #1034

@MaxGabriel

Description

@MaxGabriel

I am interested in having WAI middleware that records the request body size and the response body size (in my case to prometheus). I don't think this exists today.

I haven't tested it, but I got this compiling which I think is roughly that. Would WAI be interested in a PR with something similar to this?

-- Largely modelled off of requestSizeCheck from wai-extra
recordRequestSize :: (Word64 -> IO ()) -> Wai.Request -> IO Wai.Request
recordRequestSize recordLength req =
  case Wai.requestBodyLength req of
    Wai.KnownLength len -> do
      recordLength len
      pure req
    Wai.ChunkedBody -> do
      currentSizeRef <- newIORef (0, False)
      let yieldNextBodyChunk :: IO ByteString
          yieldNextBodyChunk = do
            bs <- Wai.getRequestBodyChunk req
            if BS.length bs == 0 -- Out of chunks
              then do
                (currentSize, hasReported :: Bool) <- readIORef currentSizeRef
                -- (Just in case someone repeatedly calls for the next chunk when there is none)
                unless hasReported do
                  atomicModifyIORef' currentSizeRef (\(prevSize, _) -> ((prevSize, True), ()))
                  recordLength currentSize
              else do
                atomicModifyIORef' currentSizeRef $ \(prevSize, reported) ->
                  let nextSize = prevSize + fromIntegral (BS.length bs)
                   in ((nextSize, reported), ())
                pure ()

            pure bs
      -- Each time a caller downstream gets the next chunk, it will call yieldNextBodyChunk
      -- Which gives + increments the counter on the request size, until it's out of chunks
      pure $ Wai.setRequestBodyChunks yieldNextBodyChunk req

recordResponseBody :: IORef Int64 -> Wai.Response -> IO Wai.Response
recordResponseBody outgoingRef response = case response of
  Wai.ResponseStream status headers streamingBody -> do
    let wrappedBody :: (Builder.Builder -> IO ()) -> IO () -> IO ()
        wrappedBody originalSend flush =
          let modifiedSend builder = do
                let chunkSize = LBS.length (Builder.toLazyByteString builder)
                atomicModifyIORef' outgoingRef (\x -> (x + chunkSize, ()))
                originalSend builder
           in streamingBody modifiedSend flush
    pure $ Wai.ResponseStream status headers wrappedBody
  Wai.ResponseBuilder _ _ builder -> do
    let total = LBS.length (Builder.toLazyByteString builder)
    atomicWriteIORef outgoingRef total
    pure response
  Wai.ResponseRaw _ _ -> pure response -- Not sure if it's doable to measure raw responses
  Wai.ResponseFile _ _ filePath _ -> do
    size <- getFileSize filePath
    atomicWriteIORef outgoingRef (fromIntegral size)
    pure response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions