Added logger

This commit is contained in:
sianida26 2024-02-28 17:50:06 +07:00
parent 48d079254e
commit 8c279b5cd7
12 changed files with 157 additions and 5 deletions

2
.env
View File

@ -1,3 +1,5 @@
DATABASE_URL=mysql://root:root@localhost:3306/dashboard_template
JWT_SECRET=
ERROR_LOG_PATH=./logs

BIN
bun.lockb

Binary file not shown.

3
env.ts
View File

@ -3,7 +3,8 @@ const envVariables = z.object({
DATABASE_URL: z.string(),
JWT_SECRET: z.string(),
WS_PORT: z.string().optional(),
WS_HOST: z.string().optional()
WS_HOST: z.string().optional(),
ERROR_LOG_PATH: z.string()
});
envVariables.parse(process.env);

1
logs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.log

View File

@ -32,6 +32,7 @@
"bcrypt": "^5.1.1",
"client-only": "^0.0.1",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"jsonwebtoken": "^9.0.2",
"mantine-form-zod-resolver": "^1.1.0",
"next": "14.1.0",

View File

@ -0,0 +1,38 @@
import BaseError from "@/core/error/BaseError";
import handleCatchApi from "@/core/utils/handleCatchApi";
import AuthError from "@/modules/auth/error/AuthError";
import signInSchema from "@/modules/auth/formSchemas/signInSchema";
import signIn from "@/modules/auth/services/signIn";
import getTokenFromHeaders from "@/modules/auth/utils/getTokenFromHeaders";
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { json } from "stream/consumers";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
if (request.headers.get("Content-Type") !== "application/json")
throw new BaseError({
errorCode: "UNSUPPORTED_CONTENT_TYPE",
message:
"This content type is not supported. Please use application/json instead",
statusCode: 400
});
const data = signInSchema.safeParse(await request.json());
if (!data.success){
throw new AuthError({
errorCode: "INVALID_CREDENTIALS",
message: "Email or Password does not match",
statusCode: 401
})
}
const result = await signIn(data.data)
return NextResponse.json(result);
} catch (e) {
return handleCatchApi(e)
}
}

View File

@ -1,20 +1,25 @@
export const BaseErrorCodes = ["UNKOWN_ERROR"] as const;
import logger from "../logger/Logger";
export const BaseErrorCodes = ["UNKNOWN_ERROR", "UNSUPPORTED_CONTENT_TYPE"] as const;
interface ErrorOptions {
message?: string;
errorCode: (typeof BaseErrorCodes)[number] | (string & {});
statusCode?: number
}
class BaseError extends Error {
public readonly errorCode: (typeof BaseErrorCodes)[number] | (string & {});
public readonly statusCode: number;
constructor(options: ErrorOptions) {
super(options.message ?? "Undetermined Error");
this.errorCode = options.errorCode ?? "UNKOWN_ERROR";
this.errorCode = options.errorCode ?? "UNKNOWN_ERROR";
this.statusCode = options.statusCode ?? 500;
Object.setPrototypeOf(this, new.target.prototype);
console.error("error:", options)
this.saveToLog();
}
getActionResponseObject() {
@ -26,6 +31,23 @@ class BaseError extends Error {
},
} as const;
}
getRestApiResponseObject(){
return {
message: this.message,
errorCode: this.errorCode
}
}
saveToLog() {
const excludedErrorCodes: string[] = [];
if (excludedErrorCodes.includes(this.errorCode)) {
return;
}
logger.error(JSON.stringify({errorCode: this.errorCode, message: this.message, stack: this.stack}))
}
}
export default BaseError;

44
src/core/logger/Logger.ts Normal file
View File

@ -0,0 +1,44 @@
import { appendFileSync } from "node:fs";
import { format } from 'date-fns';
class Logger {
logDirectory: string;
readonly severityLevels = {
CRITICAL: 'CRITICAL',
ERROR: 'ERROR',
WARNING: 'WARNING',
};
constructor(){
this.logDirectory = `${process.env.ERROR_LOG_PATH}`
}
getLogFileName(isError = false) {
// Use a different naming convention for error logs if needed
const suffix = isError ? '-error' : '';
return `${format(new Date(), 'yyyy-MM-dd')}${suffix}.log`;
}
log(message: string, level = 'info') {
const timestamp = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
const isError = level === this.severityLevels.ERROR || level === this.severityLevels.CRITICAL || level === this.severityLevels.WARNING;
const logFilePath = `${this.logDirectory}/${this.getLogFileName(isError)}`;
appendFileSync(logFilePath, logMessage);
}
error(message: string, severity = 'ERROR') {
// Ensure the severity level is valid; default to 'ERROR' if not
if (!Object.values(this.severityLevels).includes(severity)) {
severity = this.severityLevels.ERROR;
}
this.log(message, severity);
}
}
const logger = new Logger();
export default logger;

View File

@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import BaseError from "../error/BaseError";
export default function handleCatchApi(e: unknown): NextResponse {
if (e instanceof BaseError) {
return NextResponse.json({
code: e.errorCode,
message: e.message,
}, {status: e.statusCode});
}
if (e instanceof Error) {
return NextResponse.json({
code: "GENERAL_ERROR",
message: e.message,
}, {status: 500});
}
return NextResponse.json({
code: "GENERAL_ERROR",
message: "Unexpected",
}, { status: 500 });
}

View File

@ -7,11 +7,13 @@ export const AuthErrorCodes = [
"INVALID_JWT_TOKEN",
"JWT_SECRET_EMPTY",
"USER_ALREADY_EXISTS",
"ALREADY_LOGGED_IN"
] as const;
interface AuthErrorOptions {
message?: string;
errorCode: (typeof AuthErrorCodes)[number] | (string & {});
statusCode?: number;
}
export default class AuthError extends BaseError {
@ -21,6 +23,7 @@ export default class AuthError extends BaseError {
super({
errorCode: options.errorCode,
message: options.message,
statusCode: options.statusCode,
});
this.errorCode = options.errorCode;

View File

@ -0,0 +1,8 @@
import { z } from "zod";
const signInSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
export default signInSchema;

View File

@ -0,0 +1,10 @@
export default function getTokenFromHeaders(headers: Headers) {
const authorizationHeader = headers.get('authorization');
if (authorizationHeader) {
const parts = authorizationHeader.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
return parts[1];
}
}
return null;
}