diff --git a/.gitignore b/.gitignore index 4b56acf..35b56df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ /build # Logs -logs +# logs # This should be allowed because of the entity called `logs` *.log npm-debug.log* pnpm-debug.log* diff --git a/src/app.module.ts b/src/app.module.ts index 36b03d4..5c1d140 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { dataSourceOptions } from './appDataSource'; import { EntryModule } from './entries/entries.module'; import { DataSource, EntityManager } from 'typeorm'; import { UsersModule } from './users/users.module'; +import { LogsModule } from './logs/logs.module'; @Module({ imports: [ @@ -15,6 +16,7 @@ import { UsersModule } from './users/users.module'; TypeOrmModule.forRoot(dataSourceOptions), EntryModule, UsersModule, + LogsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/appDataSource.ts b/src/appDataSource.ts index ad436c5..cf2119a 100644 --- a/src/appDataSource.ts +++ b/src/appDataSource.ts @@ -4,6 +4,8 @@ import { Initial1770020781006 } from './migrations/1770020781006-initial'; import { AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 } from './migrations/1770067407619-add_create_update_delete_cols_and_rename_source_to_sourceText'; import { User } from './users/users.entity'; import { AddUserEntity1770147074119 } from './migrations/1770147074119-add_user_entity'; +import { Log } from './logs/logs.entity'; +import { AddLogEntity1770232943778 } from './migrations/1770232943778-add_log_entity'; export const dataSourceOptions: DataSourceOptions = { type: 'postgres', @@ -12,11 +14,12 @@ export const dataSourceOptions: DataSourceOptions = { username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, - entities: [Entry, User], + entities: [Entry, User, Log], migrations: [ Initial1770020781006, AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619, - AddUserEntity1770147074119 + AddUserEntity1770147074119, + AddLogEntity1770232943778, ], synchronize: false, }; diff --git a/src/entries/entries.module.ts b/src/entries/entries.module.ts index 2125f7a..a31d160 100644 --- a/src/entries/entries.module.ts +++ b/src/entries/entries.module.ts @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Entry } from './entries.entity'; import { EntryService } from './entries.service'; import { EntriesController } from './entries.controller'; +import { LogsModule } from 'src/logs/logs.module'; @Module({ - imports: [TypeOrmModule.forFeature([Entry])], + imports: [TypeOrmModule.forFeature([Entry]), LogsModule], exports: [EntryService], providers: [EntryService], controllers: [EntriesController], diff --git a/src/entries/entries.service.ts b/src/entries/entries.service.ts index 53bed95..7f71fab 100644 --- a/src/entries/entries.service.ts +++ b/src/entries/entries.service.ts @@ -1,20 +1,21 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Entry } from './entries.entity'; import { Repository } from 'typeorm'; import { EntryDTO } from './entries.dto'; import EntryNotFoundException from './exceptions/entryNotFound.exception'; +import { LogsService } from 'src/logs/logs.service'; @Injectable() export class EntryService { constructor( @InjectRepository(Entry) private entryRepository: Repository, + private readonly logger: LogsService, ) {} - private readonly logger = new Logger(EntryService.name); async save(data: EntryDTO): Promise { - this.logger.log('Creating entry'); + await this.logger.log('Creating entry', 'EntryService'); const lEntry = new Entry(); lEntry.description = data.description; lEntry.sourceText = data.sourceText; @@ -25,7 +26,7 @@ export class EntryService { } async updateByUuid(uuid: string, data: EntryDTO): Promise { - this.logger.log(`Updating entry with UUID ${uuid}`); + await this.logger.log(`Updating entry with UUID ${uuid}`, 'EntryService'); const oldEntry = await this.entryRepository.findOneBy({ uuid: uuid }); if (!oldEntry) { throw new EntryNotFoundException(uuid); @@ -44,38 +45,56 @@ export class EntryService { } async findOneByUuid(uuid: string): Promise { - this.logger.log(`Returning entry with UUID ${uuid}`); + await this.logger.log(`Returning entry with UUID ${uuid}`, 'EntryService'); return this.entryRepository.findOneByOrFail({ uuid: uuid }); } async findAll(): Promise { - this.logger.log('Returning all entries'); + await this.logger.log('Returning all entries', 'EntryService'); return this.entryRepository.find(); } async findAllAndDeleted(): Promise { - this.logger.log('Returning all entries, active and deleted'); + await this.logger.log( + 'Returning all entries, active and deleted', + 'EntryService', + ); return this.entryRepository.find({ withDeleted: true }); } async softDeleteByUuid(uuid: string): Promise { - this.logger.log(`Soft deleting entry with UUID ${uuid}`); + await this.logger.log( + `Soft deleting entry with UUID ${uuid}`, + 'EntryService', + ); const deleteResponse = await this.entryRepository.softDelete({ uuid: uuid, }); if (!deleteResponse.affected) { - throw new EntryNotFoundException(uuid); + const exception = new EntryNotFoundException(uuid); + await this.logger.error( + exception.message, + exception.stack, + 'EntryService', + ); + throw exception; } } async restoreDeletedByUuid(uuid: string): Promise { - this.logger.log(`Restoring entry with UUID ${uuid}`); + await this.logger.log(`Restoring entry with UUID ${uuid}`, 'EntryService'); const restoreResponse = await this.entryRepository.restore({ uuid: uuid, }); if (!restoreResponse.affected) { - throw new EntryNotFoundException(uuid); + const exception = new EntryNotFoundException(uuid); + await this.logger.error( + exception.message, + exception.stack, + 'EntryService', + ); + throw exception; } else { return this.entryRepository.findOneByOrFail({ uuid: uuid, diff --git a/src/logs/logs.entity.ts b/src/logs/logs.entity.ts new file mode 100644 index 0000000..275ca7c --- /dev/null +++ b/src/logs/logs.entity.ts @@ -0,0 +1,24 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'logs' }) +export class Log { + @PrimaryGeneratedColumn() + id: number; + + @Column() + level: string; // 'log', 'error', 'warn', etc. + + @Column({ type: String }) + message: string; + + @Column({ type: String, nullable: true }) + context?: string; + + @CreateDateColumn() + timestamp: Date; +} diff --git a/src/logs/logs.module.ts b/src/logs/logs.module.ts new file mode 100644 index 0000000..d3844e5 --- /dev/null +++ b/src/logs/logs.module.ts @@ -0,0 +1,12 @@ +// src/logs/logs.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Log } from './logs.entity'; +import { LogsService } from './logs.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Log])], + exports: [LogsService], + providers: [LogsService], +}) +export class LogsModule {} diff --git a/src/logs/logs.service.ts b/src/logs/logs.service.ts new file mode 100644 index 0000000..634550a --- /dev/null +++ b/src/logs/logs.service.ts @@ -0,0 +1,46 @@ +import { Injectable, Logger, LoggerService } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Log } from './logs.entity'; + +@Injectable() +export class LogsService extends Logger implements LoggerService { + constructor( + @InjectRepository(Log) + private readonly logRepository: Repository, + ) { + super(); + } + + async log(message: string, context?: string) { + // output to console using default Logger + super.log(message, context); + // Save in DB using TypeORM + await this.saveLog('log', message, context); + } + + async error(message: string, stack?: string, context?: string) { + super.error(message, stack, context); + await this.saveLog('error', `${message} - ${stack || ''}`, context); + } + + async warn(message: string, context?: string) { + super.warn(message, context); + await this.saveLog('warn', message, context); + } + + async debug(message: string, context?: string) { + super.debug(message, context); + await this.saveLog('debug', message, context); + } + + private async saveLog(level: string, message: string, context?: string) { + try { + const log = this.logRepository.create({ level, message, context }); + await this.logRepository.save(log); + } catch (err) { + // maybe have a backup log (local file?) to catch these + console.error('Failed to save log to database:', err); + } + } +} diff --git a/src/migrations/1770232943778-add_log_entity.ts b/src/migrations/1770232943778-add_log_entity.ts new file mode 100644 index 0000000..edb5800 --- /dev/null +++ b/src/migrations/1770232943778-add_log_entity.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddLogEntity1770232943778 implements MigrationInterface { + name = 'AddLogEntity1770232943778'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "logs" ("id" SERIAL NOT NULL, "level" character varying NOT NULL, "message" character varying NOT NULL, "context" character varying, "timestamp" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_fb1b805f2f7795de79fa69340ba" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "logs"`); + } +}