notes.dt.in.th

Request and response logging in Elysia

One of the criteria of whether I consider a web framework to be production grade or not is whether it has a built-in logger1. Unfortunately, Elysia does not come with any built-in logger2. Although community plugins are available, there are at least 4 options that you have to choose from.

However, for more control, you may want to create your own logger, or maybe you already have a logger that you want to integrate with Elysia. As of writing, this is not well-documented yet, and the ingredients are spread across multiple pages in the Elysia documentation, so here goes: To set up logging, you can create a plugin that hooks into the onRequest and onAfterResponse life cycle events:

import Elysia from 'elysia'

export function createLogger() {
  return new Elysia()
    .onRequest(({ request }) => {
      // … do something when request begins …
    })
    .onAfterResponse(({ request, response }) => {
      // … do something after response is sent …
    })
    .as('plugin')
}

For example, here is how to integrate Hono's built-in logger with Elysia:

import Elysia from 'elysia'
import { logger } from 'hono/logger'

export function createLogger() {
  const log = logger()
  interface RequestState {
    // Emulates the requested context object for Hono's logger
    c: {
      req: { raw: Request; method: string }
      res?: Response
    }
    resolve: () => void
  }

  const stateMap = new WeakMap<Request, RequestState>()
  return new Elysia()
    .onRequest(({ request }) => {
      let resolve: () => void
      const state: RequestState = {
        c: { req: { raw: request, method: request.method } },
        resolve: () => resolve(),
      }
      const promise = new Promise<void>((r) => (resolve = r))
      stateMap.set(request, state)
      log(state.c as any, () => promise)
    })
    .onAfterResponse(({ request, response }) => {
      const state = stateMap.get(request)
      if (!state) return
      state.c.res = response as Response
      state.resolve()
    })
    .as('plugin')
}

Another example, this time integrating with unjs' consola logger:

import { consola } from 'consola'
import Elysia from 'elysia'
import { getPath } from 'hono/utils/url'

export function createLogger() {
  const map = new WeakMap<Request, { prefix: string; start: number }>()
  return new Elysia()
    .onRequest(({ request }) => {
      const state = {
        prefix: `[${request.method}] ${getPath(request)}`,
        start: performance.now(),
      }
      consola.start(state.prefix)
      map.set(request, state)
    })
    .onAfterResponse(({ request, response: r }) => {
      const state = map.get(request)
      if (!state) return
      const response = r as Response
      const time = `${Math.round(performance.now() - state.start)}ms`
      if (response.status >= 400) {
        consola.fail(state.prefix, response.status, time)
      } else {
        consola.success(state.prefix, response.status, time)
      }
    })
    .as('plugin')
}

Footnotes

  1. Some examples:

  2. To its credit, Elysia does have an official integration for OpenTelemetry.