Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ api/fetch_cache
api/postgres_db
api/meilisearch_db
api/nodemon.json
api/logs

# web
bundle
Expand Down
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@dzcode.io/data": "*",
"@dzcode.io/models": "*",
"@dzcode.io/utils": "*",
"@omdxp/jslog": "^1.7.1",
"@sentry/node": "^8.28.0",
"@sentry/profiling-node": "^8.28.0",
"@types/make-fetch-happen": "^10.0.4",
Expand All @@ -31,8 +32,7 @@
"postgres": "^3.4.4",
"reflect-metadata": "^0.2.2",
"routing-controllers": "^0.10.4",
"typedi": "^0.10.0",
"winston": "^3.3.3"
"typedi": "^0.10.0"
},
"devDependencies": {
"@dzcode.io/tooling": "*",
Expand Down
4 changes: 2 additions & 2 deletions api/src/ai/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type OpenAIResponse = {
export class AIService {
constructor(
private readonly configService: ConfigService,
private readonly logger: LoggerService,
private readonly loggerService: LoggerService,
private readonly fetchService: FetchService,
private readonly aiPromptRepository: AiPromptRepository,
) {}
Expand All @@ -44,7 +44,7 @@ export class AIService {

const body = { model: "gpt-4o", messages: payloadWithValidationPrompt };

this.logger.info({ message: "Cached response not found, querying AI..." });
this.loggerService.logger.info("Cached response not found, querying AI...");
// todo-zm: change to captureEvent
captureException("AI Query", { tags: { type: "CRON" }, extra: { body } });

Expand Down
29 changes: 26 additions & 3 deletions api/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,36 @@ useContainer(Container); // eslint-disable-line react-hooks/rules-of-hooks
};
const app: Application = createExpressServer(routingControllersOptions);

const logger = Container.get(LoggerService);
const loggerService = Container.get(LoggerService);

Sentry.setupExpressErrorHandler(app);

// Graceful shutdown handler for logger file streams
const shutdown = (signal: string, server?: ReturnType<Application["listen"]>) => {
loggerService.logger.info("Received signal, closing logger streams", "signal", signal);
if (server) {
server.close(() => {
loggerService.close();
process.exit(0);
});
// Force shutdown after timeout
setTimeout(() => {
loggerService.logger.error("Forced shutdown after timeout");
loggerService.close();
process.exit(1);
}, 10000);
} else {
loggerService.close();
process.exit(0);
}
};

// Start it
app.listen(PORT, () => {
const server = app.listen(PORT, () => {
const commonConfig = fsConfig(NODE_ENV);
logger.info({ message: `API Server up on: ${commonConfig.api.url}/` });
loggerService.logger.info("API Server started", "url", commonConfig.api.url);
});

process.on("SIGTERM", () => shutdown("SIGTERM", server));
process.on("SIGINT", () => shutdown("SIGINT", server));
})();
33 changes: 27 additions & 6 deletions api/src/app/middlewares/logger.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
import { RequestHandler } from "express";
import { ExpressMiddlewareInterface, Middleware } from "routing-controllers";
import { LoggerService, LogLevel } from "src/logger/service";
import { LoggerService } from "src/logger/service";
import { Service } from "typedi";
import { HttpReq, HttpRes, Level } from "@omdxp/jslog";

@Service()
@Middleware({ type: "after" })
export class LoggerMiddleware implements ExpressMiddlewareInterface {
constructor(private loggerService: LoggerService) {}

use: RequestHandler = (req, res, next) => {
let logLevel: LogLevel = "info";
let logLevel = Level.INFO;
const { statusCode } = res;
if (statusCode < 100 && statusCode >= 400) {
logLevel = "error";

if (statusCode >= 500) {
logLevel = Level.ERROR;
} else if (statusCode >= 400) {
logLevel = Level.WARN;
} else if (statusCode >= 300) {
logLevel = Level.DEBUG;
}

this.loggerService.log(logLevel, {
message: `${res.statusCode} ${req.method} ${req.url}`,
const logger = this.loggerService.logger;
const message = `${req.method} ${req.url}`;

const requestAttrs = HttpReq({
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.headers["user-agent"],
});

const responseAttrs = HttpRes({
status: statusCode,
});

const logAttrs = [...requestAttrs, ...responseAttrs];

logger.log(logLevel, message, ...logAttrs);

next();
};
}
17 changes: 7 additions & 10 deletions api/src/digest/cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class DigestCron {
private isRunning = false;

constructor(
private readonly logger: LoggerService,
private readonly loggerService: LoggerService,
private readonly dataService: DataService,
private readonly githubService: GithubService,
private readonly projectsRepository: ProjectRepository,
Expand All @@ -64,7 +64,7 @@ export class DigestCron {
this.schedule,
async () => {
if (this.isRunning) {
logger.warn({ message: "Digest cron already running" });
loggerService.logger.warn("Digest cron already running");
return;
}

Expand All @@ -75,10 +75,7 @@ export class DigestCron {
this.isRunning = false;
console.error(error);
captureException(error, { tags: { type: "CRON" } });
logger.error({
message: `Digest cron failed: ${error}`,
meta: { error },
});
loggerService.logger.error("Digest cron failed", "error", error);
}
this.isRunning = false;
},
Expand All @@ -90,20 +87,20 @@ export class DigestCron {
undefined,
true,
);
logger.info({ message: "Digest cron initialized" });
loggerService.logger.info("Digest cron initialized");
}

/**
* Generate a random runId, use it to tag all newly fetched data, persist it to the database, then delete all data that doesn't have that runId.
*/
private async run() {
const runId = Math.random().toString(36).slice(2);
this.logger.info({ message: `Digest cron started, runId: ${runId}` });
this.loggerService.logger.info("Digest cron started", "runId", runId);

let projectsFromDataFolder = await this.dataService.listProjects();

if (this.configService.env().NODE_ENV === "development") {
this.logger.info({ message: `Running in development mode, filtering projects` });
this.loggerService.logger.info("Running in development mode, filtering projects");
projectsFromDataFolder = projectsFromDataFolder.filter((p) =>
["Open-listings", "dzcode.io website", "Mishkal", "System Monitor"].includes(p.name),
);
Expand Down Expand Up @@ -326,7 +323,7 @@ it may contain non-translatable parts like acronyms, keep them as is.`;
captureException(error, { tags: { type: "CRON" } });
}

this.logger.info({ message: `Digest cron finished, runId: ${runId}` });
this.loggerService.logger.info("Digest cron finished", "runId", runId);
}

private async getRepoInfo(
Expand Down
6 changes: 3 additions & 3 deletions api/src/fetch/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { FetchConfig } from "./types";
export class FetchService {
constructor(
private readonly configService: ConfigService,
private readonly logger: LoggerService,
private readonly loggerService: LoggerService,
) {
const { FETCH_CACHE_PATH } = this.configService.env();

Expand Down Expand Up @@ -46,10 +46,10 @@ export class FetchService {

private makeFetchHappenInstance;
private async fetch<T>(url: string, options: FetchOptions) {
this.logger.info({ message: `Fetching ${url}` });
this.loggerService.logger.info("Fetching URL", "url", url);
const response = await this.makeFetchHappenInstance(url, options);
if (!response.ok) {
this.logger.error({ message: `Failed to fetch ${url}`, meta: { status: response.status } });
this.loggerService.logger.error("Failed to fetch URL", "url", url, "status", response.status);
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
const jsonResponse = (await response.json()) as T;
Expand Down
83 changes: 52 additions & 31 deletions api/src/logger/service.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,66 @@
import { Service } from "typedi";
import winston from "winston";
import {
Logger,
New,
MultiHandler,
PrettyHandler,
ColorHandler,
JSONHandler,
FileHandler,
Level,
} from "@omdxp/jslog";
import * as path from "path";
import * as fs from "fs";

@Service()
export class LoggerService {
private _logger: Logger;
private fileHandler?: FileHandler;

constructor() {
this.logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [
new winston.transports.Console({
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
}),
],
});
}
const isDev = process.env.NODE_ENV === "development";
const logDir = path.join(process.cwd(), "logs");

public log(level: LogLevel, logInfo: LogObject) {
this.logger.log(level, logInfo.message, logInfo.meta);
}
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}

public info(logInfo: LogObject) {
this.log("info", logInfo);
}
if (isDev) {
const devHandler = new PrettyHandler({
handler: new ColorHandler({
level: Level.DEBUG,
addSource: true,
}),
indent: 2,
compactArrays: true,
});
this._logger = New(devHandler);
} else {
this.fileHandler = new FileHandler({
filepath: path.join(logDir, "api.log"),
maxSize: 50 * 1024 * 1024,
maxFiles: 10,
format: "json",
level: Level.INFO,
addSource: false,
});

public error(logInfo: LogObject) {
this.log("error", logInfo);
}
const productionHandler = new MultiHandler([
new JSONHandler({ level: Level.INFO }),
this.fileHandler,
]);

public debug(logInfo: LogObject) {
this.log("debug", logInfo);
this._logger = New(productionHandler);
}
}

public warn(logInfo: LogObject) {
this.log("warn", logInfo);
public get logger(): Logger {
return this._logger;
}

private logger;
public close(): void {
if (this.fileHandler) {
this.fileHandler.close();
}
}
}

export type LogLevel = "info" | "error" | "debug" | "warn";
type LogObject = {
message: string;
meta?: unknown;
};
8 changes: 4 additions & 4 deletions api/src/postgres/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ export class PostgresService {
private readonly configService: ConfigService,
private readonly loggerService: LoggerService,
) {
this.loggerService.info({ message: "Initializing Postgres database" });
this.loggerService.logger.info("Initializing Postgres database");
const { POSTGRES_URI } = this.configService.env();

const queryClient = postgres(POSTGRES_URI);
this.drizzleDB = drizzle(queryClient);
this.loggerService.info({ message: "Database migration started" });
this.loggerService.logger.info("Database migration started");
}

public async migrate() {
if (this.isReady) throw new Error("Database is already ready");

this.loggerService.info({ message: "Database migration started" });
this.loggerService.logger.info("Database migration started");
await migrate(this.drizzleDB, { migrationsFolder: join(__dirname, "../../db/migrations") });
this.loggerService.info({ message: "Database migration complete" });
this.loggerService.logger.info("Database migration complete");

this.isReady = true;
}
Expand Down
Loading
Loading