Added custom logger that logs to db

This commit is contained in:
2026-02-04 16:06:49 -05:00
parent 3d8d23ed69
commit 68f43b9bdf
9 changed files with 137 additions and 15 deletions

2
.gitignore vendored
View File

@@ -4,7 +4,7 @@
/build /build
# Logs # Logs
logs # logs # This should be allowed because of the entity called `logs`
*.log *.log
npm-debug.log* npm-debug.log*
pnpm-debug.log* pnpm-debug.log*

View File

@@ -8,6 +8,7 @@ import { dataSourceOptions } from './appDataSource';
import { EntryModule } from './entries/entries.module'; import { EntryModule } from './entries/entries.module';
import { DataSource, EntityManager } from 'typeorm'; import { DataSource, EntityManager } from 'typeorm';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { LogsModule } from './logs/logs.module';
@Module({ @Module({
imports: [ imports: [
@@ -15,6 +16,7 @@ import { UsersModule } from './users/users.module';
TypeOrmModule.forRoot(dataSourceOptions), TypeOrmModule.forRoot(dataSourceOptions),
EntryModule, EntryModule,
UsersModule, UsersModule,
LogsModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@@ -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 { AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619 } from './migrations/1770067407619-add_create_update_delete_cols_and_rename_source_to_sourceText';
import { User } from './users/users.entity'; import { User } from './users/users.entity';
import { AddUserEntity1770147074119 } from './migrations/1770147074119-add_user_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 = { export const dataSourceOptions: DataSourceOptions = {
type: 'postgres', type: 'postgres',
@@ -12,11 +14,12 @@ export const dataSourceOptions: DataSourceOptions = {
username: process.env.DB_USERNAME, username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
entities: [Entry, User], entities: [Entry, User, Log],
migrations: [ migrations: [
Initial1770020781006, Initial1770020781006,
AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619, AddCreateUpdateDeleteColsAndRenameSourceToSourceText1770067407619,
AddUserEntity1770147074119 AddUserEntity1770147074119,
AddLogEntity1770232943778,
], ],
synchronize: false, synchronize: false,
}; };

View File

@@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Entry } from './entries.entity'; import { Entry } from './entries.entity';
import { EntryService } from './entries.service'; import { EntryService } from './entries.service';
import { EntriesController } from './entries.controller'; import { EntriesController } from './entries.controller';
import { LogsModule } from 'src/logs/logs.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Entry])], imports: [TypeOrmModule.forFeature([Entry]), LogsModule],
exports: [EntryService], exports: [EntryService],
providers: [EntryService], providers: [EntryService],
controllers: [EntriesController], controllers: [EntriesController],

View File

@@ -1,20 +1,21 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Entry } from './entries.entity'; import { Entry } from './entries.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { EntryDTO } from './entries.dto'; import { EntryDTO } from './entries.dto';
import EntryNotFoundException from './exceptions/entryNotFound.exception'; import EntryNotFoundException from './exceptions/entryNotFound.exception';
import { LogsService } from 'src/logs/logs.service';
@Injectable() @Injectable()
export class EntryService { export class EntryService {
constructor( constructor(
@InjectRepository(Entry) @InjectRepository(Entry)
private entryRepository: Repository<Entry>, private entryRepository: Repository<Entry>,
private readonly logger: LogsService,
) {} ) {}
private readonly logger = new Logger(EntryService.name);
async save(data: EntryDTO): Promise<Entry> { async save(data: EntryDTO): Promise<Entry> {
this.logger.log('Creating entry'); await this.logger.log('Creating entry', 'EntryService');
const lEntry = new Entry(); const lEntry = new Entry();
lEntry.description = data.description; lEntry.description = data.description;
lEntry.sourceText = data.sourceText; lEntry.sourceText = data.sourceText;
@@ -25,7 +26,7 @@ export class EntryService {
} }
async updateByUuid(uuid: string, data: EntryDTO): Promise<Entry> { async updateByUuid(uuid: string, data: EntryDTO): Promise<Entry> {
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 }); const oldEntry = await this.entryRepository.findOneBy({ uuid: uuid });
if (!oldEntry) { if (!oldEntry) {
throw new EntryNotFoundException(uuid); throw new EntryNotFoundException(uuid);
@@ -44,38 +45,56 @@ export class EntryService {
} }
async findOneByUuid(uuid: string): Promise<Entry | null> { async findOneByUuid(uuid: string): Promise<Entry | null> {
this.logger.log(`Returning entry with UUID ${uuid}`); await this.logger.log(`Returning entry with UUID ${uuid}`, 'EntryService');
return this.entryRepository.findOneByOrFail({ uuid: uuid }); return this.entryRepository.findOneByOrFail({ uuid: uuid });
} }
async findAll(): Promise<Entry[]> { async findAll(): Promise<Entry[]> {
this.logger.log('Returning all entries'); await this.logger.log('Returning all entries', 'EntryService');
return this.entryRepository.find(); return this.entryRepository.find();
} }
async findAllAndDeleted(): Promise<Entry[]> { async findAllAndDeleted(): Promise<Entry[]> {
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 }); return this.entryRepository.find({ withDeleted: true });
} }
async softDeleteByUuid(uuid: string): Promise<void> { async softDeleteByUuid(uuid: string): Promise<void> {
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({ const deleteResponse = await this.entryRepository.softDelete({
uuid: uuid, uuid: uuid,
}); });
if (!deleteResponse.affected) { 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<Entry | null> { async restoreDeletedByUuid(uuid: string): Promise<Entry | null> {
this.logger.log(`Restoring entry with UUID ${uuid}`); await this.logger.log(`Restoring entry with UUID ${uuid}`, 'EntryService');
const restoreResponse = await this.entryRepository.restore({ const restoreResponse = await this.entryRepository.restore({
uuid: uuid, uuid: uuid,
}); });
if (!restoreResponse.affected) { if (!restoreResponse.affected) {
throw new EntryNotFoundException(uuid); const exception = new EntryNotFoundException(uuid);
await this.logger.error(
exception.message,
exception.stack,
'EntryService',
);
throw exception;
} else { } else {
return this.entryRepository.findOneByOrFail({ return this.entryRepository.findOneByOrFail({
uuid: uuid, uuid: uuid,

24
src/logs/logs.entity.ts Normal file
View File

@@ -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;
}

12
src/logs/logs.module.ts Normal file
View File

@@ -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 {}

46
src/logs/logs.service.ts Normal file
View File

@@ -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<Log>,
) {
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);
}
}
}

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddLogEntity1770232943778 implements MigrationInterface {
name = 'AddLogEntity1770232943778';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE "logs"`);
}
}