import { Injectable, NotFoundException, Optional } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { PaginatedResponse } from '@app/common/paginated-response.dto'; import { CreatePlaybackErrorLogDto } from './logs.dto'; import { PlaybackErrorLog } from './playback-error-log.entity'; import { RequestLog } from './request-log.entity'; import { PlaybackErrorLogRecord, RequestLogRecord, } from './logs.types'; interface PaginationInput { page: number; pageSize: number; } @Injectable() export class LogsService { private requestLogSequence = 1; private playbackErrorSequence = 1; private readonly requestLogs = new Map(); private readonly playbackErrors: PlaybackErrorLogRecord[] = []; constructor( @Optional() @InjectDataSource() readonly dataSource?: DataSource, ) {} async recordRequest(record: Omit) { if (!record.requestId) { return; } if (this.hasDatabase()) { const repository = this.dataSource.getRepository(RequestLog); const existing = await repository.findOne({ where: { requestId: record.requestId }, }); await repository.save( repository.create({ ...existing, ...record, }), ); return; } this.requestLogs.set(record.requestId, { ...record, id: this.requestLogs.get(record.requestId)?.id ?? this.requestLogSequence++, createdAt: this.requestLogs.get(record.requestId)?.createdAt ?? new Date().toISOString(), }); } async listRequestLogs( pagination: PaginationInput, ): Promise> { if (this.hasDatabase()) { const page = Math.max(pagination.page, 1); const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100); const repository = this.dataSource.getRepository(RequestLog); const [records, total] = await repository.findAndCount({ order: { id: 'DESC' }, skip: (page - 1) * pageSize, take: pageSize, }); return { list: records.map((record) => this.toRequestLogRecord(record)), pagination: { page, pageSize, total }, }; } const list = Array.from(this.requestLogs.values()).sort( (left, right) => (right.id ?? 0) - (left.id ?? 0), ); return this.paginate(list, pagination); } async getRequestLogOrThrow(requestId: string) { if (this.hasDatabase()) { const log = await this.dataSource .getRepository(RequestLog) .findOne({ where: { requestId } }); if (!log) { throw new NotFoundException('Request log not found'); } return this.toRequestLogRecord(log); } const log = this.requestLogs.get(requestId); if (!log) { throw new NotFoundException('Request log not found'); } return log; } async createPlaybackError(dto: CreatePlaybackErrorLogDto) { if (this.hasDatabase()) { const repository = this.dataSource.getRepository(PlaybackErrorLog); const log = await repository.save( repository.create({ ...dto, occurredAt: new Date(dto.occurredAt), }), ); return { id: Number(log.id), requestId: log.requestId, }; } const log: PlaybackErrorLogRecord = { id: this.playbackErrorSequence++, requestId: dto.requestId, dramaId: dto.dramaId, episodeId: dto.episodeId, playUrl: dto.playUrl, playerErrorCode: dto.playerErrorCode, message: dto.message, progressSeconds: dto.progressSeconds, networkType: dto.networkType, device: dto.device, occurredAt: dto.occurredAt, createdAt: new Date().toISOString(), }; this.playbackErrors.push(log); return { id: log.id, requestId: log.requestId, }; } async listPlaybackErrors( pagination: PaginationInput, ): Promise> { if (this.hasDatabase()) { const page = Math.max(pagination.page, 1); const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100); const repository = this.dataSource.getRepository(PlaybackErrorLog); const [records, total] = await repository.findAndCount({ order: { id: 'DESC' }, skip: (page - 1) * pageSize, take: pageSize, }); return { list: records.map((record) => this.toPlaybackErrorLogRecord(record)), pagination: { page, pageSize, total }, }; } return this.paginate([...this.playbackErrors].reverse(), pagination); } async getPlaybackErrorOrThrow(id: number) { if (this.hasDatabase()) { const log = await this.dataSource .getRepository(PlaybackErrorLog) .findOne({ where: { id } }); if (!log) { throw new NotFoundException('Playback error log not found'); } return this.toPlaybackErrorLogRecord(log); } const log = this.playbackErrors.find((item) => item.id === id); if (!log) { throw new NotFoundException('Playback error log not found'); } return log; } private hasDatabase(): this is this & { dataSource: DataSource } { return Boolean(this.dataSource?.isInitialized); } private toRequestLogRecord(log: RequestLog): RequestLogRecord { return { id: Number(log.id), requestId: log.requestId, method: log.method, path: log.path, query: log.query, body: log.body, statusCode: log.statusCode, responseCode: log.responseCode, message: log.message, costMs: log.costMs, userId: log.userId ? Number(log.userId) : undefined, adminId: log.adminId ? Number(log.adminId) : undefined, ip: log.ip, userAgent: log.userAgent, errorStack: log.errorStack, createdAt: log.createdAt?.toISOString?.(), }; } private toPlaybackErrorLogRecord(log: PlaybackErrorLog): PlaybackErrorLogRecord { return { id: Number(log.id), requestId: log.requestId, userId: log.userId ? Number(log.userId) : undefined, dramaId: Number(log.dramaId), episodeId: Number(log.episodeId), playUrl: log.playUrl, playerErrorCode: log.playerErrorCode, message: log.message, progressSeconds: log.progressSeconds, networkType: log.networkType, device: log.device, occurredAt: log.occurredAt.toISOString(), createdAt: log.createdAt?.toISOString?.() ?? new Date().toISOString(), }; } private paginate( records: T[], input: PaginationInput, ): PaginatedResponse { const page = Math.max(input.page, 1); const pageSize = Math.min(Math.max(input.pageSize, 1), 100); const start = (page - 1) * pageSize; return { list: records.slice(start, start + pageSize), pagination: { page, pageSize, total: records.length, }, }; } }