feat: add logging and error handling
This commit is contained in:
parent
f324ca45ff
commit
56c0fed144
10 changed files with 720 additions and 49 deletions
|
|
@ -1,6 +1,9 @@
|
|||
import * as path from "path";
|
||||
import * as uuid from "uuid";
|
||||
import { Express } from "express";
|
||||
import * as express from "express";
|
||||
import * as winston from "winston";
|
||||
import * as morgan from "morgan";
|
||||
import { Connection, createConnection } from "mysql2/promise";
|
||||
import { engine as handlebarsEngine } from "express-handlebars";
|
||||
|
||||
|
|
@ -15,34 +18,62 @@ export class Armory {
|
|||
public dbc: DbcManager;
|
||||
public config: Config;
|
||||
public worldDb: Connection;
|
||||
public logger: winston.Logger;
|
||||
|
||||
private charsDbs: { [key: string]: Connection };
|
||||
private errorNames: { [key: number]: string };
|
||||
private errorDescriptions: { [key: number]: string };
|
||||
|
||||
public constructor() {
|
||||
this.dbc = new DbcManager();
|
||||
this.characterCustomization = new CharacterCustomization();
|
||||
this.charsDbs = {};
|
||||
this.logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
|
||||
winston.format.printf((info) => `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`),
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({ level: "debug" }),
|
||||
new winston.transports.File({ filename: "armory.error.log", level: "error" }),
|
||||
new winston.transports.File({ filename: "armory.combined.log", level: "http" }),
|
||||
],
|
||||
});
|
||||
|
||||
this.errorNames = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
500: "Internal Server Error",
|
||||
};
|
||||
this.errorDescriptions = {
|
||||
400: "Invalid request.",
|
||||
404: "Sorry, we could not find what you were looking for.",
|
||||
500: "An unexpected internal error has occurred. Please contact the site owner.",
|
||||
};
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
const app: Express = express();
|
||||
const listenPort = 48733;
|
||||
|
||||
console.log("Loading config...");
|
||||
this.config = await Config.load();
|
||||
console.log("Loading data files...");
|
||||
this.logger.info("Loading config...");
|
||||
this.config = await Config.load(this.logger);
|
||||
this.logger.info("Loading data files...");
|
||||
if (this.config.loadDbcs) {
|
||||
await this.dbc.loadAllFiles();
|
||||
}
|
||||
await this.characterCustomization.loadData();
|
||||
|
||||
console.log("Connecting to databases...");
|
||||
this.logger.info("Connecting to databases...");
|
||||
this.worldDb = await createConnection(this.config.worldDatabase);
|
||||
for (const realm of this.config.realms) {
|
||||
this.charsDbs[realm.name.toLowerCase()] = await createConnection(realm.charactersDatabase);
|
||||
}
|
||||
|
||||
console.log("Starting server...");
|
||||
this.logger.info("Starting server...");
|
||||
app.locals.aowow = this.config.aowowUrl;
|
||||
app.engine(".html", handlebarsEngine({
|
||||
extname: "html",
|
||||
|
|
@ -54,6 +85,20 @@ export class Armory {
|
|||
app.set("view engine", "handlebars");
|
||||
app.set("views", path.join(process.cwd(), "static"));
|
||||
|
||||
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
req.id = uuid.v4();
|
||||
next();
|
||||
});
|
||||
|
||||
morgan.token("id", (req: express.Request) => {
|
||||
return req.id;
|
||||
});
|
||||
app.use(morgan(":method :url :status - ID :id - :remote-addr - :response-time ms", {
|
||||
stream: {
|
||||
write: (msg) => this.logger.http(msg.trim()),
|
||||
},
|
||||
}));
|
||||
|
||||
app.use("/js", express.static(`static/js`));
|
||||
app.use("/css", express.static(`static/css`));
|
||||
app.use("/img", express.static(`static/img`));
|
||||
|
|
@ -64,19 +109,55 @@ export class Armory {
|
|||
app.use("/data/background.png", express.static(`data/background.png`));
|
||||
|
||||
const indexController = new IndexController(this);
|
||||
app.get("/", indexController.index.bind(indexController));
|
||||
app.get("/search", indexController.search.bind(indexController));
|
||||
app.get("/", this.wrapRoute(indexController.index.bind(indexController)));
|
||||
app.get("/search", this.wrapRoute(indexController.search.bind(indexController)));
|
||||
|
||||
const charsController = new CharacterController(this);
|
||||
await charsController.load();
|
||||
app.get("/character/:realm/:name", charsController.character.bind(charsController));
|
||||
app.get("/character/:realm/:name/talents", charsController.talents.bind(charsController));
|
||||
app.get("/character/:realm/:name/achievements", charsController.achievements.bind(charsController));
|
||||
app.get("/character/:realm/:character/achievements/data", charsController.achievementsData.bind(charsController));
|
||||
app.get("/character/:realm/:name", this.wrapRoute(charsController.character.bind(charsController)));
|
||||
app.get("/character/:realm/:name/talents", this.wrapRoute(charsController.talents.bind(charsController)));
|
||||
app.get("/character/:realm/:name/achievements", this.wrapRoute(charsController.achievements.bind(charsController)));
|
||||
app.get("/character/:realm/:character/achievements/data", this.wrapRoute(charsController.achievementsData.bind(charsController)));
|
||||
|
||||
app.use((err, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// Error handler
|
||||
|
||||
if (err instanceof Error) {
|
||||
const contents = err.stack ?? `${err.name}: ${err.message}`;
|
||||
this.logger.error(`Error on request ${req.id}. ${contents}`);
|
||||
}
|
||||
|
||||
let status = 500;
|
||||
if (typeof err === "number") {
|
||||
status = err;
|
||||
}
|
||||
|
||||
res.status(status).render("error.html", this.getErrorViewData(status, req));
|
||||
});
|
||||
|
||||
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// 404 handler
|
||||
res.status(404);
|
||||
|
||||
// Respond with html page
|
||||
if (req.accepts("html")) {
|
||||
res.render("error.html", this.getErrorViewData(404, req));
|
||||
return;
|
||||
}
|
||||
|
||||
// Respond with json
|
||||
if (req.accepts("json")) {
|
||||
res.json({ error: this.errorNames[404] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to plain-text
|
||||
res.type("txt").send(this.errorNames[404]);
|
||||
});
|
||||
|
||||
this.gc();
|
||||
app.listen(listenPort, "0.0.0.0", () => {
|
||||
console.log(`Server is listening on 0.0.0.0:${listenPort}.`);
|
||||
this.logger.info(`Server is listening on 0.0.0.0:${listenPort}.`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -95,4 +176,24 @@ export class Armory {
|
|||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private wrapRoute(fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<any>) {
|
||||
// Adds error handling for promise-based controller methods
|
||||
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
try {
|
||||
await fn(req, res, next);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getErrorViewData(status: number, req: express.Request) {
|
||||
return {
|
||||
status,
|
||||
name: this.errorNames[status] || "An error occurred",
|
||||
description: this.errorDescriptions[status] || "",
|
||||
reqId: req.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import * as fs from "fs";
|
||||
const fsp = fs.promises;
|
||||
|
||||
import * as winston from "winston";
|
||||
|
||||
export interface IDatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
|
|
@ -25,31 +27,31 @@ export class Config {
|
|||
|
||||
private static checkedMissingField: boolean = false;
|
||||
|
||||
public static async load(): Promise<Config> {
|
||||
public static async load(logger: winston.Logger): Promise<Config> {
|
||||
const json: Buffer = await fsp.readFile("config.json");
|
||||
const config = JSON.parse(json.toString()) as Config;
|
||||
|
||||
if (!Config.checkedMissingField) {
|
||||
const defaultConfigJson = await fsp.readFile("config.default.json");
|
||||
const defaultConfig = JSON.parse(defaultConfigJson.toString());
|
||||
Config.checkAllMissingFields(config, defaultConfig);
|
||||
Config.checkAllMissingFields(logger, config, defaultConfig);
|
||||
Config.checkedMissingField = true;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private static checkAllMissingFields(obj: object, model: object, parentName: string = "") {
|
||||
private static checkAllMissingFields(logger: winston.Logger, obj: object, model: object, parentName: string = "") {
|
||||
const missing = Config.hasMissingFields(obj, model);
|
||||
if (parentName !== "") {
|
||||
parentName += ".";
|
||||
}
|
||||
for (const field of missing) {
|
||||
console.warn(`Field ${parentName}${field} is missing in config.json!`);
|
||||
logger.warn(`Field ${parentName}${field} is missing in config.json!`);
|
||||
}
|
||||
for (const key in model) {
|
||||
if (typeof model[key] === "object" && obj.hasOwnProperty(key)) {
|
||||
Config.checkAllMissingFields(obj[key], model[key], parentName + key);
|
||||
Config.checkAllMissingFields(logger, obj[key], model[key], parentName + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,22 +147,20 @@ export class CharacterController {
|
|||
}
|
||||
}
|
||||
|
||||
public async character(req: express.Request, res: express.Response): Promise<void> {
|
||||
public async character(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
const realm = this.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const equipmentData = await this.getEquipmentData(realmName, charData.guid);
|
||||
|
|
@ -193,22 +191,20 @@ export class CharacterController {
|
|||
this.armory.gc();
|
||||
}
|
||||
|
||||
public async talents(req: express.Request, res: express.Response): Promise<void> {
|
||||
public async talents(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
const realm = this.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
res.render("character-talents.html", {
|
||||
|
|
@ -222,22 +218,20 @@ export class CharacterController {
|
|||
});
|
||||
}
|
||||
|
||||
public async achievements(req: express.Request, res: express.Response): Promise<void> {
|
||||
public async achievements(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const charName = req.params.name;
|
||||
|
||||
const realm = this.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, charName);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
res.render("character-achievements.html", {
|
||||
|
|
@ -246,22 +240,20 @@ export class CharacterController {
|
|||
});
|
||||
}
|
||||
|
||||
public async achievementsData(req: express.Request, res: express.Response): Promise<void> {
|
||||
public async achievementsData(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.params.realm;
|
||||
const character = parseInt(req.params.character) || -1;
|
||||
|
||||
const realm = this.getRealm(realmName);
|
||||
if (realm === undefined) {
|
||||
// Could not find realm
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
const charData = await this.getCharacterData(realm, character);
|
||||
if (charData === null) {
|
||||
// Could not find character
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
return next(404);
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -44,14 +44,13 @@ export class IndexController {
|
|||
});
|
||||
}
|
||||
|
||||
public async search(req: express.Request, res: express.Response): Promise<void> {
|
||||
public async search(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
|
||||
const realmName = req.query.realm as string;
|
||||
const realm = realmName === undefined ?
|
||||
this.armory.config.realms[0] :
|
||||
this.armory.config.realms.find(r => r.name === realmName);
|
||||
if (realm === undefined) {
|
||||
res.status(400);
|
||||
return;
|
||||
return next(400);
|
||||
}
|
||||
|
||||
const db = this.armory.getCharactersDb(realm.name);
|
||||
|
|
|
|||
5
src/index.d.ts
vendored
Normal file
5
src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
declare module Express {
|
||||
export interface Request {
|
||||
id: string;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue