Access log middleware for Deno Fresh

khuongduybui

Duy K. Bui

Posted on October 16, 2022

Access log middleware for Deno Fresh

TL/DR: If you read the title and know exactly what I mean already, simply go to https://deno.land/x/fresh_logging and follow the README.

This is part 3 of a series about my Deno Fresh plugins, so I will not reiterate on the topics already defined in part 1. If you are not interested in my Cloudflare Turnstile plugin, please at least read part 1's Background section if you are unsure what Deno Fresh is.

Background

I was looking for a Deno middleware to help me easily handle the request's Accept header and couldn't find any. As a good citizen of the open-source community, I set out to create one myself and started reading about Deno middlewares. As soon as I read the official docs, however, I realized what the community is missing: an access log middleware! (Side note: as a security software engineer by day, it's my occupational habit to care about access logs.)

As of this post's writing, my plugin supports the two most commonly used (in my daily work) access log formats: the W3C Common Log Format and a variant by Apache called Combined Log Format.

Prerequisites

By now you should already know the drill.

Create a Fresh app if you haven't.

deno run -A -r https://fresh.deno.dev sample-app
Enter fullscreen mode Exit fullscreen mode

Now that you have created your Fresh app, add fresh-logging to your import_map.json for convenience:

{
  "imports": {
    "$logging/": "https://deno.land/x/fresh_logging@1.1.2/"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then consume the middleware in your app's routes/_middleware.ts.

import * as getLogger from "$logging/index.ts";
// or
// import { getLogger, ... } from "$logging/index.ts";

export const handler = [
  getLogger(),
  // ... other middlewares
];
Enter fullscreen mode Exit fullscreen mode

Use Case 1: Common Log Format

You do not have to do anything, this is the default format.

Use Case 2: Apache Combined Log Format

Specify LoggingFormat.APACHE_COMBINED for the format option like this:

import { getLogger, LoggingFormat } from "$logging/index.ts";

export const handler = [
  getLogger({
    format: LoggingFormat.APACHE_COMBINED,
  }),
];
Enter fullscreen mode Exit fullscreen mode

The default two headers included are Referer and User-agent. You can override that by optionally providing the combinedHeaders option, which expects a string array of length 2.

Limitations

As of v1.1.2, the following fields are completely omitted (hard-coded to -):

  • rfc931 (client identifier): not sure how to obtain this
  • authuser (user identifier): not sure how to obtain this either
  • bytes (response content length): one way I can think of is to use res.clone() then read its as ArrayBuffer and get the byteLength, but that is both time and memory consuming. Until I can find a more efficient way to obtain this piece of information, omission is the decision.

Users can use the resolvers to provide custom resolutions of the missing fields. For example, the following code snippet allows logging the response bytes:

import { getLogger, ResolutionField } from "$logging/index.ts";

export const handler = [
  getLogger({
    resolvers: {
      [ResolutionField.bytes]: async (_req, _ctx, res) => `${(await res.clone().arrayBuffer()).byteLength}`,
    },
  }),
];
Enter fullscreen mode Exit fullscreen mode

Again, please note that the example above only serves to illustrate how to provide customer resolvers for the missing fields, the actual implementation is sub-optimal. Otherwise, it would have been included as default resolver for that field.

Additional Features

Response Time

Regardless of log format, you can set the option includeDuration to true to include the handler response time at the end of each log entry. The header Server-Timing will also be set on the response.

import { getLogger } from "$logging/index.ts";

export const handler = [
  getLogger({includeDuration: true}),
  // ... other middlewares
];
Enter fullscreen mode Exit fullscreen mode

Note: if includeDuration option is ON, getLogger() will also count the time taken by all of its subsequent middlewares.

For example, putting getLogger() at the beginning of your handler array will count the time taken by all middlewares, while putting it at the very end of your handler array will yield the time taken only by the route handler.

Custom Logger

By default, the middleware uses an Optic logger that sends output to the console at INFO level, with the color, but without the INFO prefix.

You can provide a log function with the signature (message: string) => string to the logger option to use your own log writing implementation.

import { getLogger } from "$logging/index.ts";

export const handler = [
  getLogger({
    logger: (message: string) => {
      console.debug(message);
      return message;
    },
  }),
];
Enter fullscreen mode Exit fullscreen mode

The Optic logging library is full of features (seriously, look at it) and is highly recommended, but you can literally choose any implementation in your custom logger function.

Custom Resolver

As briefly discussed in the Limitations section above, you can provide a custom resolver for fields that the middleware is unsure how to populate. The following code snippet illustrates how to resolve the authuser field.


import { getLogger, ResolutionField } from "$logging/index.ts";

const auth: MiddlewareHandler = (req: Request, ctx: MiddlewareHandlerContext) => {
  ctx.state.user = "-";
  if (req.headers.has("Authorization")) {
    // inspect req.headers.get("Authorization") and override ctx.state.user
  }
  return ctx.next();
};

export const handler = [
  auth,
  getLogger({
    resolvers: {
      [ResolutionField.authuser]: (_req, ctx, _res) => ctx.state.user as string,
    },
  }),
];
Enter fullscreen mode Exit fullscreen mode

Appendix: Options

The getLogger() function accepts an optional object {} with the following options:

Option Default Value Notes
format LoggingFormat.COMMON The log format to use, defaulting to the W3C Common Log Format.
utcTime false Whether to log timestamps in UTC or server timezone.
includeDuration false Whether to include handler response time.
resolvers {} Selectively supply customer resolvers for the missing fields. See the section on limitations for more details.
logger console.info +color -level Optionally supply a custom logger function of type (message: string) => string. See the section on custom logger for more details.
combinedHeaders ["Referer", "User-agent"] Optionally supply custom request headers to include. Requires setting format: LoggingFormat.APACHE_COMBINED.

That's it, my friends. Thank you for staying with me till the end of it. Feel free to start a discussion or file an issue! You may also hit me up on dev.to messaging anytime too!

💖 💪 🙅 🚩
khuongduybui
Duy K. Bui

Posted on October 16, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related