Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 12 additions & 2 deletions api/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,23 @@ 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) => {
loggerService.logger.info("Received signal, closing logger streams", "signal", signal);
loggerService.close();
process.exit(0);
};

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

// Start it
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);
});
})();
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