Skip to content

Instantly share code, notes, and snippets.

@justinnoel
Last active June 13, 2023 22:42
Show Gist options
  • Save justinnoel/50593d105a515a6f23c7e1c608c220cd to your computer and use it in GitHub Desktop.
Save justinnoel/50593d105a515a6f23c7e1c608c220cd to your computer and use it in GitHub Desktop.
Send logs to external service in a Cloudflare Remix project
type LogRemote = {
details: {
message: string,
},
env: {
REMOTE_LOGGING_URL: string,
REMOTE_LOGGING_KEY: string,
REMOTE_LOGGING_SOURCE: string,
},
};
export async function logRemote({ details, env }: LogRemote) {
// NOTE: Do not remove. This is for logging on server side for access via Cloudflare logs
console.log(`${JSON.stringify(details)}`);
try {
return fetch(`${env.REMOTE_LOGGING_URL}=${env.REMOTE_LOGGING_SOURCE}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": env.REMOTE_LOGGING_KEY,
},
body: JSON.stringify(details),
});
} catch (error) {
console.log(`${details.message} : Failed to log remotely!`);
}
}
import { logRemote } from "./logRemote";
export type Log = {
data?: {},
error?: any,
message?: string,
method: string,
status: "start" | "success" | "fail" | "info",
tags?: string[],
ts: number,
};
export const logger = async (context: any, response: Response) => {
const excludes = [
".css",
".ico",
"/fonts/",
".js",
".png",
".jpg",
".jpeg",
".svg",
".wepb",
];
const hasExcludedString = excludes.some((excludedString) => {
return context.request.url.indexOf(excludedString) > 0;
});
if (hasExcludedString) {
await context.next();
return;
}
const url = new URL(context.request.url);
const pathName = url.pathname;
const searchParams = url.searchParams;
const status = response.status;
const { duration, privateMessage, requestId, start } = context.requestDetails;
const statusToLog = status === 200 ? "OK" : privateMessage;
const message = `${context.requestDetails.requestId} :: ${
context?.env?.APPLICATION_NAME ?? "Missing APPLICATION_NAME"
} :: ${start} :: ${
context.request.method
} ${pathName}?${searchParams} - ${duration}ms :: ${status} :: ${
statusToLog || ""
} :: logs: ${context.logs.length}`;
const { city, continent, country, postalCode, region } =
context?.request?.cf ?? {};
const badStatuses = [401, 405];
// Log Additional info for all unauthorized requests
const additionalLogs = badStatuses.includes(status)
? {
ipAddress: context?.request?.headers?.get("cf-connecting-ip"),
cfRay: context?.request?.headers?.get("cf-ray"),
cfRequestId: context?.request?.headers?.get("cf-request-id"),
}
: undefined;
const logDetails = {
message,
metadata: {
duration: duration,
logs: context.logs,
method: context.method,
path: pathName,
privateMessage: privateMessage,
requestId,
status: status,
statusMessage: statusToLog,
tags: [context?.env?.APPLICATION_NAME ?? "Missing APPLICATION_NAME"],
ts: start,
ua: context?.request?.headers?.get("user-agent"),
cf: {
city: city,
continent,
country,
postalCode,
region,
},
securityInfo: { ...additionalLogs },
},
};
context.waitUntil(logRemote({ details: logDetails, env: context.env }));
};
export const processTimer = async (context: any) => {
context.requestDetails.start = Date.now();
await context.next();
context.requestDetails.stop = Date.now();
context.requestDetails.duration =
context.requestDetails.stop - context.requestDetails.start;
};
import type { ActionFunction } from "remix";
import { definitions } from "~/types/database";
import { getSupabase } from "~/lib/supabase/supabase.server";
export const notesAction: ActionFunction = async ({ context, request }) => {
const logs = context.logs;
logs.push({
method: "notesAction",
ts: Date.now(),
status: "start",
});
// TODO: Replace with `getPostData` function
const formData = await request.formData();
const { _action, ...values } = Object.fromEntries(formData);
const supabase = getSupabase({ context, service_role: true });
// Make sure the notes are not excessively wrong indicating abuse
// TODO: Fix this typing
const notes = values?.notes?.substring(0, 5000);
const { error } =
(await supabase.from) <
definitions["notes"] >
"notes".insert({ note: notes });
if (error) {
logs.push({
method: "notesAction",
ts: Date.now(),
status: "fail",
message: "Failed to add notes",
error: error,
});
return null;
}
logs.push({
method: "notesAction",
ts: Date.now(),
status: "success",
});
return null;
};
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "@remix-run/dev/server-build";
import { logger, processTimer } from "~/utils/logger";
import { extendContext } from "~/utils/extendContext";
import { redirect } from "remix";
const handleRequest = createPagesFunctionHandler({
build,
mode: (context) => {
return context;
},
getLoadContext: (context) => {
return context;
},
});
export async function onRequest(context) {
try {
extendContext(context);
processTimer(context);
const response = await handleRequest(context);
response.headers?.set("X-Request-Id", context?.requestDetails.requestId);
logger(context, response);
return response;
} catch (error) {
console.error("server OnRequuest Error");
console.log(error);
console.log(error.message);
return redirect("/auth");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment