Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: errors to be handed by throwing the HTTPException class #38

Merged
merged 6 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cors } from "hono/cors";
import { compress } from "hono/compress";
import { csrf } from "hono/csrf";
import { etag } from "hono/etag";
import { HTTPException } from "hono/http-exception";
import { secureHeaders } from "hono/secure-headers";
import { timeout } from "hono/timeout";
import { logger } from "hono/logger";
Expand Down Expand Up @@ -32,9 +33,8 @@ const limiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100,
standardHeaders: "draft-6",
keyGenerator: (c) => c.req.header("x-app-service-id") ?? c.req.header("x-forwarded-for") ?? "",
keyGenerator: (c) => c.req.header("CF-Connecting-IP") ?? c.req.header("x-forwarded-for") ?? "",
});
app.use(limiter);

app.use("*", async (c, next) => {
c.set("service", null);
Expand All @@ -45,6 +45,8 @@ app.use("*", async (c, next) => {
app.use("/api/", timeout(5000));

app.route("/api/v2", v2Router);

app.use(limiter);
app.route("/docs", docsRouter);

app.get(
Expand All @@ -61,6 +63,16 @@ app.get("/", (c) => {
return c.redirect("/docs");
});

app.onError(function handleError(err, c) {
if (err instanceof HTTPException) {
const response = err.getResponse();
return response;
}

c.status(500);
return c.json({ success: false, message: "Unknown Internal Server Error" });
});

if (env.FREEZE_DB_WRITES) {
console.warn("\n🚨 Database writes are currently frozen!!!\n");
}
Expand Down
17 changes: 15 additions & 2 deletions src/routers/v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";

import type { ServerContext } from "@/types/hono";

import servicesRouter from "./services";
import logsRouter from "./logging";

import type { ServerContext } from "@/types/hono";

const app = new Hono<ServerContext>();

const limiter = rateLimiter({
windowMs: 5 * 60 * 1000, // 5 minutes
limit: 100,
standardHeaders: "draft-6",
keyGenerator: (c) => {
return (
c.req.header("x-app-service-id") ?? c.req.header("CF-Connecting-IP") ?? c.req.header("x-forwarded-for") ?? ""
);
},
});
app.use(limiter);

app.route("/service", servicesRouter);
app.route("/log", logsRouter);

Expand Down
149 changes: 80 additions & 69 deletions src/routers/v2/logging/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
import { and, eq, lt } from "drizzle-orm";

import { db } from "@/config/db";
import { env } from "@/config/env";
import { logs as logsTable } from "@/config/db/schema";
import { parseSearchParams, v2_serviceValidation } from "@/utils/server-helpers";
import { createV2ErrResponse, parseSearchParams, v2_serviceValidation } from "@/utils/server-helpers";
import { ENDPOINT_MESSAGES } from "@/utils/messages";
import { createDbId } from "@/utils/db";
import type { ServerContext } from "@/types/hono";
Expand All @@ -25,28 +26,31 @@ app.get("/", v2_serviceValidation, async (c) => {
const searchResult = getLogsFiltersSchema.safeParse(searchQuery);

if (!searchResult.success) {
c.status(400);
return c.json({ success: false, message: searchResult.error.message });
throw new HTTPException(400, { res: createV2ErrResponse(searchResult.error.message) });
}

const search = searchResult.data;

const logLevels = search.level.filter((val) => val !== "all");

const logs = await db.query.logs.findMany({
limit: search.page_size,
offset: search.page_size * (search.page - 1),
orderBy: (fields, { asc, desc }) => (search.sort === "ASC" ? asc(fields.createdAt) : desc(fields.createdAt)),
where: (fields, { and, eq, inArray }) =>
and(
...[eq(fields.serviceId, serviceId)],
...(search.environment ? [eq(fields.environment, search.environment)] : []),
...(search.lookup ? [eq(fields.lookupFilterValue, search.lookup)] : []),
...(logLevels.length > 0 ? [inArray(fields.level, logLevels)] : []),
),
});

return c.json(getLogsOutputSchema.parse(logs));
try {
const logs = await db.query.logs.findMany({
limit: search.page_size,
offset: search.page_size * (search.page - 1),
orderBy: (fields, { asc, desc }) => (search.sort === "ASC" ? asc(fields.createdAt) : desc(fields.createdAt)),
where: (fields, { and, eq, inArray }) =>
and(
...[eq(fields.serviceId, serviceId)],
...(search.environment ? [eq(fields.environment, search.environment)] : []),
...(search.lookup ? [eq(fields.lookupFilterValue, search.lookup)] : []),
...(logLevels.length > 0 ? [inArray(fields.level, logLevels)] : []),
),
});

return c.json(getLogsOutputSchema.parse(logs));
} catch (error) {
throw new HTTPException(500, { res: createV2ErrResponse(ENDPOINT_MESSAGES.InternalError) });
}
});

/**
Expand All @@ -66,40 +70,43 @@ app.post("/", v2_serviceValidation, async (c) => {
const bodyResult = createLogSchema.safeParse(body);

if (!bodyResult.success) {
c.status(400);
return c.json({ success: false, message: bodyResult.error.message });
throw new HTTPException(400, { res: createV2ErrResponse(bodyResult.error.message) });
}

const input = bodyResult.data;

const logId = createDbId("log");
const log = await db
.insert(logsTable)
.values({
id: logId,
serviceId,
action: input.action,
environment: input.environment,
ip: input.ip,
data: input.data || {},
isPersisted: service.isPersisted,
lookupFilterValue: input.lookupFilterValue,
level: input.level,
})
.returning({
id: logsTable.id,
action: logsTable.id,
environment: logsTable.environment,
ip: logsTable.ip,
lookupFilterValue: logsTable.lookupFilterValue,
data: logsTable.data,
level: logsTable.level,
createdAt: logsTable.createdAt,
})
.execute();

c.status(201);
return c.json(createLogOutputSchema.parse(log[0]));
try {
const logId = createDbId("log");
const log = await db
.insert(logsTable)
.values({
id: logId,
serviceId,
action: input.action,
environment: input.environment,
ip: input.ip,
data: input.data || {},
isPersisted: service.isPersisted,
lookupFilterValue: input.lookupFilterValue,
level: input.level,
})
.returning({
id: logsTable.id,
action: logsTable.id,
environment: logsTable.environment,
ip: logsTable.ip,
lookupFilterValue: logsTable.lookupFilterValue,
data: logsTable.data,
level: logsTable.level,
createdAt: logsTable.createdAt,
})
.execute();

c.status(201);
return c.json(createLogOutputSchema.parse(log[0]));
} catch (error) {
throw new HTTPException(500, { res: createV2ErrResponse(ENDPOINT_MESSAGES.InternalError) });
}
});

/**
Expand All @@ -121,28 +128,32 @@ app.delete("/purge", v2_serviceValidation, async (c) => {
const date = new Date();
date.setMonth(date.getMonth() - countMonths);

await db
.delete(logsTable)
.where(and(eq(logsTable.isPersisted, false), lt(logsTable.createdAt, date.toISOString())))
.execute();

const logId = createDbId("log");
await db
.insert(logsTable)
.values({
id: logId,
serviceId: service.id,
isPersisted: true,
action: "app-admin-clean-service-logs",
ip,
environment: "production",
lookupFilterValue: "app-admin-action",
data: { client: service.name, ip },
level: "info",
})
.execute();

return c.json({ success: true, message: `Successfully cleaned logs for the last ${countMonths} months` });
try {
await db
.delete(logsTable)
.where(and(eq(logsTable.isPersisted, false), lt(logsTable.createdAt, date.toISOString())))
.execute();

const logId = createDbId("log");
await db
.insert(logsTable)
.values({
id: logId,
serviceId: service.id,
isPersisted: true,
action: "app-admin-clean-service-logs",
ip,
environment: "production",
lookupFilterValue: "app-admin-action",
data: { client: service.name, ip },
level: "info",
})
.execute();

return c.json({ success: true, message: `Successfully cleaned logs for the last ${countMonths} months` });
} catch (error) {
throw new HTTPException(500, { res: createV2ErrResponse(ENDPOINT_MESSAGES.InternalError) });
}
});

export default app;
Loading