pino-http icon indicating copy to clipboard operation
pino-http copied to clipboard

Unable to log `query` nor `params` for request serializers

Open samholmes opened this issue 2 years ago • 15 comments

app.use(
    pinoMiddleware({
      logger,
      customLogLevel: res => {
        return res.statusCode >= 500 ? 'warn' : 'info'
      },
      serializers: {
        req: numbRequest,
        res: numbResponse
      }
    })
  )
export const numbRequest = (req: any) => {
  const { url, repoId } = numbEndpoint(req.url)
  const { params, query } = req

  const out = {
    id: req.id,
    method: req.method,
    url,
    params,
    query,
    foo: 'hi',
    remoteAddress: req.remoteAddress,
    remotePort: req.remotePort,
    repoId
  }

  console.log('query', query)
  console.log('JSON.stringify(query)', JSON.stringify(query))
  console.log('params', params)
  console.log('JSON.stringify(params)', JSON.stringify(params))
  console.log('out', out)

  return out
}

The query and params console.log output is showing in the terminal, however the fields are not showing up in the final log output. The field foo is showing, so this means that params and query are treated in a special way that is not documented.

samholmes avatar Sep 08 '21 22:09 samholmes

Could you please provide a complete example to reproduce your problem?

mcollina avatar Sep 09 '21 09:09 mcollina

Could you please provide a complete example to reproduce your problem?

It turns out my problem was a misunderstanding of the API. The serializer functions always expect the value being logged as the first parameter. I was expecting the express request object as it is from a route handler, but instead I got a the “context object” (this) of the event handlers for the finish/error events on the request object. In order to access the request object details such as query or params I would need to go through req.raw.

Figuring this out took longer than it would have if there was stronger typing for the serializers for pino-http. I understand how the typing of serializer functions could be challenging due to their dynamic nature. So this leads me to an alternative API idea: what if the library just gave you the request, response and error objects in a callback, and then you decide everything that should be logged from that callback?

samholmes avatar Sep 09 '21 16:09 samholmes

how would that API look like?

mcollina avatar Sep 09 '21 16:09 mcollina

how would that API look like?

pinoMiddleware({
  logger,
  onResFinished: (req, res, err) => {
	return { ... } // Return whatever you want from the req, res, or err objects.
  }
})

The object returned from the onResFinished callback would be included in the log which could then be serialized further. The signature of the callback is identical to that of a route handler (very familiar). The name could be even one of onResponseFinished, onRequestFinished, onFinished, or event logHandler.

samholmes avatar Sep 09 '21 16:09 samholmes

This could be done by changing:

function onResFinished (err) {
    this.removeListener('error', onResFinished)
    this.removeListener('finish', onResFinished)

    var log = this.log
    var responseTime = Date.now() - this[startTime]
    var level = customLogLevel ? customLogLevel(this, err) : useLevel

    if (err || this.err || this.statusCode >= 500) {
      var error = err || this.err || new Error('failed with status code ' + this.statusCode)

      log[level]({
        [resKey]: this,
        [errKey]: error,
        [responseTimeKey]: responseTime
      }, errorMessage(error, this))
      return
    }

    log[level]({
      [resKey]: this,
      [responseTimeKey]: responseTime
    }, successMessage(this))
  }

Into:

function onResFinished (err) {
    this.removeListener('error', onResFinished)
    this.removeListener('finish', onResFinished)

    var log = this.log
    var responseTime = Date.now() - this[startTime]
    var level = customLogLevel ? customLogLevel(this, err) : useLevel
    var logOut = {
      [resKey]: this,
      [responseTimeKey]: responseTime,
    }

    if (err || this.err || this.statusCode >= 500) {
      var error = err || this.err || new Error('failed with status code ' + this.statusCode)
      logOut[errKey] = error
      logOut.msg = errorMessage(error, this),
    }
    else {
      logOut.msg = successMessage(this),
    }

    log[level]({
      ...logOut
      ...logHandler(req, res, err)
    })
  }

Here we're going with the name logHandler as the field name for the options argument. We'll need to make onResFinished close over the loggerMiddleware scope so it has access to req, res, and err.

samholmes avatar Sep 09 '21 16:09 samholmes

I don't think we have req available at that point.

mcollina avatar Sep 10 '21 13:09 mcollina

I don't think we have req available at that point.

loggerMiddleware has req.

samholmes avatar Sep 10 '21 13:09 samholmes

Indeed. The code is written so that we avoid allocating a new closure for each request.

mcollina avatar Sep 10 '21 13:09 mcollina

If memory footprint is a concern, then we could leak the abstraction: Pass a ctx object to the log logHandler(ctx) and then allow for a field for making the context object.

samholmes avatar Sep 10 '21 18:09 samholmes

That would still add something somewhere :/.

I'm not sure what @jsumners thinks, but I'm kind of ok to how this module works now.

mcollina avatar Sep 10 '21 21:09 mcollina

@mcollina I see; so you want to avoid creating any closure whatsoever per request. I'm assuming this is because closures have significant performance implications. Regardless, use of a closure is only an implementation detail. You could get a bit more hacky and just add the req, res, and err objects from the middleware to the event emitter and access these pieces of data from this within the onResFinished. The field could be something like this._pinoHttpCtx even. The point is that you just provide a reference to those objects to a handler like logHandler to generate the log. This way you have full control over what gets logged at the end of a request.

samholmes avatar Sep 11 '21 01:09 samholmes

That would still add something somewhere :/.

I'm not sure what @jsumners thinks, but I'm kind of ok to how this module works now.

Honestly, I've never actually used this module. And I haven't really followed the code in this thread. I get the impression there is a misunderstanding of what the module is and how it works, but, again, I haven't really been paying much attention here.

jsumners avatar Sep 11 '21 02:09 jsumners

@samholmes I think your view of what you would like to achieve with this module is quite different from the current scope of it.

I have not used this in a while as well and I would prefer not to significantly change this.

mcollina avatar Sep 11 '21 08:09 mcollina

Fair enough. My only suggestion then is somehow addressing the slight lack of clarity for using the API through documentation. One thing that wasn’t clear at first was that pino-http logged a specific object that could then be changed either through serializer functions or some other handlers. Documenting what exactly this object is would be helpful. 🙂

samholmes avatar Sep 11 '21 20:09 samholmes

This issue still persists as of April 2024. Indeed, when making a request using parameters, it resolves to a URL rather than to params. Even when using custom serializers, the information that reaches the serializer about the request is already resolved to a URL, even though in the request (for example in Express.js), it belongs to the params attribute.

Log in Express.js:

params: { userId: '123' },
query: {},

Log in Pino:

  "url": "/user/123",
  "query": {},
  "params": {},

UderMoreiraDeAssis avatar Apr 12 '24 21:04 UderMoreiraDeAssis