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
# Logs
logs
# logs # This should be allowed because of the entity called `logs`
*.log
npm-debug.log*
pnpm-debug.log*

View File

@@ -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],

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

View File

@@ -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],

View File

@@ -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<Entry>,
private readonly logger: LogsService,
) {}
private readonly logger = new Logger(EntryService.name);
async save(data: EntryDTO): Promise<Entry> {
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<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 });
if (!oldEntry) {
throw new EntryNotFoundException(uuid);
@@ -44,38 +45,56 @@ export class EntryService {
}
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 });
}
async findAll(): Promise<Entry[]> {
this.logger.log('Returning all entries');
await this.logger.log('Returning all entries', 'EntryService');
return this.entryRepository.find();
}
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 });
}
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({
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<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({
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,

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"`);
}
}