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
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/"
}
}
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
];
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,
}),
];
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 useres.clone()
then read its asArrayBuffer
and get thebyteLength
, 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}`,
},
}),
];
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
];
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;
},
}),
];
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,
},
}),
];
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!
Top comments (0)