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:
parent
ce985ef8f8
commit
0f976edf96
9 changed files with 91 additions and 18 deletions
|
@ -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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
37
server/src/middleware/http-exception.filter.ts
Normal file
37
server/src/middleware/http-exception.filter.ts
Normal 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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}]`) + ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue