Awesome Logging in Sveltekit

Jeff McMorris
5 min readMay 26, 2023
Data dashboards can be created from the captured logging data

I wanted to create a way to log events in my app. I wanted it to be easy, yet powerful. I wanted to know if functions were running slow and I wanted to capture any errors my app would have. I also didn’t want it to be difficult to to track something, since I am lazy:-)

I use the awesome logging service Axiom which has a very generous free tier to capture my log data, but you could use a different service if you like or even no service if you just want to use it in dev mode.

After watching a couple of great tutorials from huntabyte and joy of code, I decided the best place to capture the log would be in the +hooks.server.ts file. That way I could capture a lot of information without having to do much of anything for most pages.

Here is all of the info I want to track.

  • level: (info or error),
  • method: GET or POST,
  • path: location of request,
  • status: http status code,
  • timeInMs: how long the request took in milliseconds
  • user: the users email address if logged in.
  • userId: the users Id if logged in.
  • referrer: the referring page. If the referrer is local I just put the referring path.
  • parameters in the url: I capture any parameters attached to the url. so if you were running a campaign you could add https://yourdomain.com/?campaign=my-ad-campaign&ad=ad-banner-1 and it would capture any of these options also.
  • message and track options: In addition I created two special ways to add events into the log. I have a track variable and a message variable. By adding event.locals.message or event.locals.track you can pass in an object or string and this data will also be tracked.
  • errors: If your app has an error, three other pieces of data are gathered. There is an event.locals.error which is a string you can pass for when an error happens that is planned for. There is also an errorId which is a unique number we pass to the user to help with troubleshooting and reviewing our logs. Finally we pass the error stack trace through events.local.errorsStackTrace.

Here is an example of what I capture for each request to the server, it includes just the basics, but could have a lot more info depending on what variables are passed in.

{"level":"info","method":"GET","path":"/dashboard","status":200,"timeInMs":70,"user":"email@domain.com","userId":"xjsjldwiieirhdls","referer":"https://google.com"}

First I needed to add some new type info to my app.d.ts for some new variables I would use to do the tracking.

//app.d.ts
declare global {
namespace App {
interface Locals {
startTimer: number;
error: string;
errorId: string;
errorStackTrace: string;
message: unknown;
track: unknown;
}
interface Error {
code?: string;
errorId?: string;
}
}
}

Next I created a file called: log.ts. I then pass in the event which comes from hook.server.ts and an http status code.

import { Client } from '@axiomhq/axiom-node';
import { AXIOM_TOKEN, AXIOM_ORG_ID, AXIOM_DATASET } from '$env/static/private';
import getAllUrlParams from '$lib/_helpers/getAllUrlParams';
import parseTrack from '$lib/_helpers/parseTrack';
import parseMessage from '$lib/_helpers/parseMessage';
import { DOMAIN } from '$lib/config/constants';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
export default async function log(statusCode: number, event) {
try {
const client = new Client({
token: AXIOM_TOKEN,
orgId: AXIOM_ORG_ID
});
let level = 'info';
if (statusCode >= 400) {
level = 'error';
}
const error = event?.locals?.error || undefined;
const errorId = event?.locals?.errorId || undefined;
const errorStackTrace = event?.locals?.errorStackTrace || undefined;
let urlParams = {};
if (event?.url?.search) {
urlParams = await getAllUrlParams(event?.url?.search);
}
let messageEvents = {};
if (event?.locals?.message) {
messageEvents = await parseMessage(event?.locals?.message);
}
let trackEvents = {};
if (event?.locals?.track) {
trackEvents = await parseTrack(event?.locals?.track);
}

let referer = event.request.headers.get('referer');
if (referer) {
const refererUrl = await new URL(referer);
const refererHostname = refererUrl.hostname;
if (refererHostname === 'localhost' || refererHostname === DOMAIN) {
referer = refererUrl.pathname;
}
} else {
referer = undefined;
}
const logData: object = {
level: level,
method: event.request.method,
path: event.url.pathname,
status: statusCode,
timeInMs: Date.now() - event?.locals?.startTimer,
user: event?.locals?.user?.email,
userId: event?.locals?.user?.userId,
referer: referer,
error: error,
errorId: errorId,
errorStackTrace: errorStackTrace,
...urlParams,
...messageEvents,
...trackEvents
};
console.log('log: ', JSON.stringify(logData));
await client.ingestEvents(AXIOM_DATASET, [logData]);
} catch (err) {
throw new Error(`Error Logger: ${JSON.stringify(err)}`);
}
}

There are three support functions for the log.ts. These convert the data to objects to be ingested by our log.ts.

//getAllUrlParams.ts
export default async function getAllUrlParams(url: string): Promise<object> {
let paramsObj = {};
try {
url = url?.slice(1); //remove leading ?
if (!url) return {}; //if no params return
paramsObj = await Object.fromEntries(await new URLSearchParams(url));
} catch (error) {
console.log('error: ', error);
}
return paramsObj;
}
//parseMessage.ts
export default async function parseMessage(message: unknown): Promise<object> {
let messageObj = {};
try {
if (message) {
if (typeof message === 'string') {
messageObj = { message: message };
} else {
messageObj = message;
}
}
} catch (error) {
console.log('error: ', error);
}
return messageObj;
}
//parseTrack.ts
export default async function parseTrack(track: unknown): Promise<object> {
let trackObj = {};
try {
if (track) {
if (typeof track === 'string') {
trackObj = { track: track };
} else {
trackObj = track;
}
}
} catch (error) {
console.log('error: ', error);
}
return trackObj;
}

hooks.server.ts is how we call our log.ts file so it executes on every server request.

//hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit';
import type { HandleServerError } from '@sveltejs/kit';
import log from '$lib/server/log';



export const handleError: HandleServerError = async ({ error, event }) => {
const errorId = crypto.randomUUID();

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
event.locals.error = error?.toString() || undefined;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
event.locals.errorStackTrace = error?.stack || undefined;
event.locals.errorId = errorId;
log(500, event);
return {
message: 'An unexpected error occurred.',
errorId
};
};


export const handle: Handle = async ({ event, resolve }) => {
const startTimer = Date.now();
event.locals.startTimer = startTimer;
const response = await resolve(event);
log(response.status, event);
return response;
};

Finally we setup a custom +error.svelte to handle our custom error page which outputs the custom errorId. This is good if you have a user having trouble you can search your logs based on this string (special thanks to this huntabyte tutorial for this idea).

<!--+error.svelte-->
<script lang="ts">
import { page } from '$app/stores';
</script>

<div>
{#if $page.status === 404}
<h1>Page Not Found.</h1>
<h3 class="mt-6"><a href="/">Go Home</a></h3>
{:else}
<h1 class="h1">Unexpected Error</h1>
<h3 class="mt-6">We're investigating the issue.</h3>
{/if}

{#if $page.error?.errorId}
<p class="mt-6">Error ID: {$page.error.errorId}</p>
{/if}
</div>

How to use the special parameters for passing in message and track. It’s very simple, in any function just pass in data like this. You can either pass a string or pass an object for each one. The special error message only takes a string but you could add this in when you catch an error.

event.locals.message = 'sign in successful';
event.locals.track = { test: 'sign-in', test2: 'another value' };
event.locals.error = 'some error message';

So very easy, even for us lazy coders;-)

If you would like to see completed github project you can checkout my sveltekit auth starter project, this logging system is a part of that.

Anyway hopefully you will find this helpful for your own projects.

--

--