1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(server): log http exceptions (#9996)

This commit is contained in:
Jason Rasmussen 2024-06-05 17:07:47 -04:00 committed by GitHub
parent ce985ef8f8
commit 0f976edf96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 91 additions and 18 deletions

View file

@ -5,51 +5,61 @@ export const errorDto = {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Authentication required', message: 'Authentication required',
correlationId: expect.any(String),
}, },
forbidden: { forbidden: {
error: 'Forbidden', error: 'Forbidden',
statusCode: 403, statusCode: 403,
message: expect.any(String), message: expect.any(String),
correlationId: expect.any(String),
}, },
wrongPassword: { wrongPassword: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: 'Wrong password', message: 'Wrong password',
correlationId: expect.any(String),
}, },
invalidToken: { invalidToken: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Invalid user token', message: 'Invalid user token',
correlationId: expect.any(String),
}, },
invalidShareKey: { invalidShareKey: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Invalid share key', message: 'Invalid share key',
correlationId: expect.any(String),
}, },
invalidSharePassword: { invalidSharePassword: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Invalid password', message: 'Invalid password',
correlationId: expect.any(String),
}, },
badRequest: (message: any = null) => ({ badRequest: (message: any = null) => ({
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: message ?? expect.anything(), message: message ?? expect.anything(),
correlationId: expect.any(String),
}), }),
noPermission: { noPermission: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: expect.stringContaining('Not found or no'), message: expect.stringContaining('Not found or no'),
correlationId: expect.any(String),
}, },
incorrectLogin: { incorrectLogin: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Incorrect email or password', message: 'Incorrect email or password',
correlationId: expect.any(String),
}, },
alreadyHasAdmin: { alreadyHasAdmin: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: 'The server already has an admin', message: 'The server already has an admin',
correlationId: expect.any(String),
}, },
}; };

View file

@ -1,7 +1,7 @@
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common'; import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
@ -15,6 +15,7 @@ import { entities } from 'src/entities';
import { AuthGuard } from 'src/middleware/auth.guard'; import { AuthGuard } from 'src/middleware/auth.guard';
import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { ErrorInterceptor } from 'src/middleware/error.interceptor';
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
import { HttpExceptionFilter } from 'src/middleware/http-exception.filter';
import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
import { repositories } from 'src/repositories'; import { repositories } from 'src/repositories';
import { services } from 'src/services'; import { services } from 'src/services';
@ -26,6 +27,7 @@ const common = [...services, ...repositories];
const middleware = [ const middleware = [
FileUploadInterceptor, FileUploadInterceptor,
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
{ provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },

View file

@ -6,6 +6,7 @@ import { Request, Response } from 'express';
import { RedisOptions } from 'ioredis'; import { RedisOptions } from 'ioredis';
import Joi from 'joi'; import Joi from 'joi';
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls'; import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
import { ImmichHeader } from 'src/dtos/auth.dto';
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
export enum TranscodePolicy { export enum TranscodePolicy {
@ -419,11 +420,11 @@ export const clsConfig: ClsModuleOptions = {
mount: true, mount: true,
generateId: true, generateId: true,
setup: (cls, req: Request, res: Response) => { setup: (cls, req: Request, res: Response) => {
const headerValues = req.headers['x-immich-cid']; const headerValues = req.headers[ImmichHeader.CID];
const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues; const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
const cid = headerValue || cls.get(CLS_ID); const cid = headerValue || cls.get(CLS_ID);
cls.set(CLS_ID, cid); cls.set(CLS_ID, cid);
res.header('x-immich-cid', cid); res.header(ImmichHeader.CID, cid);
}, },
}, },
}; };

View file

@ -19,6 +19,7 @@ export enum ImmichHeader {
SESSION_TOKEN = 'x-immich-session-token', SESSION_TOKEN = 'x-immich-session-token',
SHARED_LINK_KEY = 'x-immich-share-key', SHARED_LINK_KEY = 'x-immich-share-key',
CHECKSUM = 'x-immich-checksum', CHECKSUM = 'x-immich-checksum',
CID = 'x-immich-cid',
} }
export enum ImmichQuery { export enum ImmichQuery {

View file

@ -0,0 +1,37 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common';
import { Response } from 'express';
import { ClsService } from 'nestjs-cls';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
constructor(
@Inject(ILoggerRepository) private logger: ILoggerRepository,
private cls: ClsService,
) {
this.logger.setContext(HttpExceptionFilter.name);
}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
this.logger.debug(`HttpException(${status}) ${JSON.stringify(exception.getResponse())}`);
let responseBody = exception.getResponse();
// unclear what circumstances would return a string
if (typeof responseBody === 'string') {
responseBody = {
error: 'Unknown',
message: responseBody,
statusCode: status,
};
}
response.status(status).json({
...responseBody,
correlationId: this.cls.getId(),
});
}
}

View file

@ -1,7 +1,21 @@
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common'; import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable, finalize } from 'rxjs'; import { Observable, finalize } from 'rxjs';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
const maxArrayLength = 100;
const replacer = (key: string, value: unknown) => {
if (key.toLowerCase().includes('password')) {
return '********';
}
if (Array.isArray(value) && value.length > maxArrayLength) {
return [...value.slice(0, maxArrayLength), `...and ${value.length - maxArrayLength} more`];
}
return value;
};
@Injectable() @Injectable()
export class LoggingInterceptor implements NestInterceptor { export class LoggingInterceptor implements NestInterceptor {
constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
@ -10,18 +24,23 @@ export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> { intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
const handler = context.switchToHttp(); const handler = context.switchToHttp();
const req = handler.getRequest(); const req = handler.getRequest<Request>();
const res = handler.getResponse(); const res = handler.getResponse<Response>();
const { method, ip, path } = req; const { method, ip, url } = req;
const start = performance.now(); const start = performance.now();
return next.handle().pipe( return next.handle().pipe(
finalize(() => { finalize(() => {
const finish = performance.now(); const finish = performance.now();
const duration = (finish - start).toFixed(2); const duration = (finish - start).toFixed(2);
const { statusCode } = res; const { statusCode } = res;
this.logger.verbose(`${method} ${path} ${statusCode} ${duration}ms ${ip}`);
this.logger.debug(`${method} ${url} ${statusCode} ${duration}ms ${ip}`);
if (req.body && Object.keys(req.body).length > 0) {
this.logger.verbose(JSON.stringify(req.body, replacer));
}
}), }),
); );
} }

View file

@ -30,17 +30,20 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository
} }
protected formatContext(context: string): string { protected formatContext(context: string): string {
let formattedContext = super.formatContext(context); let prefix = LoggerRepository.appName || '';
if (context) {
prefix += (prefix ? ':' : '') + context;
}
const correlationId = this.cls?.getId(); const correlationId = this.cls?.getId();
if (correlationId && this.isLevelEnabled(LogLevel.VERBOSE)) { if (correlationId) {
formattedContext += `[${correlationId}] `; prefix += `~${correlationId}`;
} }
if (LoggerRepository.appName) { if (!prefix) {
formattedContext = LogColor.blue(`[${LoggerRepository.appName}] `) + formattedContext; return '';
} }
return formattedContext; return LogColor.yellow(`[${prefix}]`) + ' ';
} }
} }

View file

@ -20,10 +20,10 @@ async function bootstrap() {
const port = Number(process.env.IMMICH_PORT) || 3001; const port = Number(process.env.IMMICH_PORT) || 3001;
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true }); const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
const logger = await app.resolve(ILoggerRepository); const logger = await app.resolve<ILoggerRepository>(ILoggerRepository);
logger.setAppName('ImmichServer'); logger.setAppName('Api');
logger.setContext('ImmichServer'); logger.setContext('Bootstrap');
app.useLogger(logger); app.useLogger(logger);
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.set('etag', 'strong'); app.set('etag', 'strong');

View file

@ -11,8 +11,8 @@ export async function bootstrap() {
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
const logger = await app.resolve(ILoggerRepository); const logger = await app.resolve(ILoggerRepository);
logger.setAppName('ImmichMicroservices'); logger.setAppName('Microservices');
logger.setContext('ImmichMicroservices'); logger.setContext('Bootstrap');
app.useLogger(logger); app.useLogger(logger);
app.useWebSocketAdapter(new WebSocketAdapter(app)); app.useWebSocketAdapter(new WebSocketAdapter(app));