Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/api.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ JWT_SECRET="123"
JWT_EXPIRES_IN="2min"
JWT_REFRESH_SECRET="321"
JWT_REFRESH_EXPIRES_IN="2days"

OPENOBSERVE_ENDPOINT="http://localhost:5080"
OPENOBSERVE_KEY="" # see key in http://localhost:5080/web/ingestion/custom/logs/curl?org_identifier=default
OPENOBSERVE_USER="[email protected]"
OPENOBSERVE_ORGANIZATION_ID="nestjs-starter-kit"
OPENOBSERVE_APP_ID="api"
5 changes: 5 additions & 0 deletions backend/apps/api/src/__mocks__/EnvConfigMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ export const EnvConfigMock: EnvConfig = {
JWT_REFRESH_SECRET: 'jwtRefreshSecret',
JWT_REFRESH_EXPIRES_IN: '2days',
DOMAIN: 'http://localhost:3000',
OPENOBSERVE_ENDPOINT: '',
OPENOBSERVE_KEY: '',
OPENOBSERVE_ORGANIZATION_ID: '',
OPENOBSERVE_APP_ID: '',
OPENOBSERVE_USER: '',
};
11 changes: 11 additions & 0 deletions backend/apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ConfigModule, Interceptor } from '@lib/core';
import { OpenobserveModule } from '@lib/logger';
import { RepositoryModule } from '@lib/repository';
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
Expand All @@ -13,6 +14,16 @@ import { UserModule } from './modules/user/user.module';
inject: [EnvConfig],
useFactory: (config: EnvConfig) => config,
}),
OpenobserveModule.registerAsync({
inject: [EnvConfig],
useFactory: (config: EnvConfig) => ({
endpoint: config.OPENOBSERVE_ENDPOINT,
key: config.OPENOBSERVE_KEY,
organizationId: config.OPENOBSERVE_ORGANIZATION_ID,
appId: config.OPENOBSERVE_APP_ID,
user: config.OPENOBSERVE_USER,
}),
}),
UserModule,
AuthModule,
],
Expand Down
15 changes: 15 additions & 0 deletions backend/apps/api/src/config/config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,19 @@ export class EnvConfig {

@IsString()
DOMAIN: string;

@IsString()
OPENOBSERVE_ENDPOINT: string;

@IsString()
OPENOBSERVE_KEY: string;

@IsString()
OPENOBSERVE_ORGANIZATION_ID: string;

@IsString()
OPENOBSERVE_APP_ID: string;

@IsString()
OPENOBSERVE_USER: string;
}
5 changes: 4 additions & 1 deletion backend/apps/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LoggerService } from '@lib/logger';
import { OpenobserveService } from '@lib/logger/modules/openobserve/openobserve.service';
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
Expand All @@ -11,8 +13,9 @@ const enableCorsByEnv = (app: INestApplication<unknown>) => {
};

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.use(cookieParser());
app.useLogger(new LoggerService(app.get(OpenobserveService)));

enableCorsByEnv(app);
const config = app.get(EnvConfig);
Expand Down
14 changes: 13 additions & 1 deletion backend/apps/api/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserNotFound, isError } from '@lib/core';
import { RepositoryService } from '@lib/repository';
import { UserEntity } from '@lib/repository/entities/user.entity';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { SHA256 } from 'crypto-js';
import { v4 } from 'uuid';
Expand All @@ -18,6 +18,8 @@ import { GetTokenResult } from './common/auth.model';

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);

constructor(
private readonly jwt: JwtService,
private readonly rep: RepositoryService,
Expand Down Expand Up @@ -96,6 +98,11 @@ export class AuthService {
user.refreshTokenHash = refreshTokenHash;
await this.rep.user.save(user);

this.logger.log({
message: 'Confirm email',
payload: { email: user.email, user: user.id, token },
});

return {
token: accessToken,
refreshCookie:
Expand All @@ -119,6 +126,11 @@ export class AuthService {
// `<a href="${link}">${link}</a>`,
// userResult.email,
// ); FIXME: Need mail service implementation

this.logger.log({
message: 'Send confirm email',
payload: { email: user.email, token, user: user.id },
});
}

private generateToken(id: number) {
Expand Down
9 changes: 8 additions & 1 deletion backend/apps/api/src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { UserNotFound } from '@lib/core';
import { RepositoryService } from '@lib/repository';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { SHA256 } from 'crypto-js';
import { UserAlreadyExists } from './common/user.errors';

@Injectable()
export class UserService {
private readonly logger = new Logger(UserService.name);

constructor(private readonly rep: RepositoryService) {}

async createUser(email: string, password: string) {
if (await this.rep.user.findOne({ where: { email } })) {
return new UserAlreadyExists(email);
}

this.logger.log({
message: 'Create user',
payload: { email },
});

const user = this.rep.user.create({ email, hash: SHA256(password).toString(), createdAt: new Date() });
await this.rep.user.save(user);

Expand Down
53 changes: 50 additions & 3 deletions backend/libs/core/src/interceptor/interceptor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class Interceptor implements NestInterceptor {

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const res: Response = context.switchToHttp().getResponse();
const req = context.switchToHttp().getRequest();

this.logger.log({
message: 'Request',
payload: {
method: req.method,
url: req.url,
user: req.user,
},
});

return next
.handle()
Expand All @@ -39,6 +49,18 @@ export class Interceptor implements NestInterceptor {

if (data.body.success) {
res.status(200);

this.logger.log({
message: 'Response',
payload: {
status: data.status,
method: req.method,
url: req.url,
body: data.body,
user: req.user,
},
});

return data.body;
}

Expand All @@ -60,19 +82,44 @@ export class Interceptor implements NestInterceptor {
status = err.getStatus();
}

this.logger.error(`INTERNAL_SERVER_ERROR Error(): ${message};\nStack: ${err.stack}`);
this.logger.error({
message: 'INTERNAL_SERVER_ERROR',
payload: {
message,
stack: err.stack,
method: req.method,
url: req.url,
user: req.user,
},
});
res.status(status);
return of(message);
}

if (err?.body && !err.body.success) {
this.logger.error(`Error: ${JSON.stringify(err)}`);
this.logger.error({
message: 'Error',
payload: {
body: err.body,
method: req.method,
url: req.url,
user: req.user,
},
});
res.status(err.status);
return of(objToString(err.body));
}

const unknownError = objToString(err);
this.logger.error(`INTERNAL_SERVER_ERROR unknown: ${unknownError}`);
this.logger.error({
message: 'UNKNOWN INTERNAL_SERVER_ERROR',
payload: {
error: unknownError,
method: req.method,
url: req.url,
user: req.user,
},
});
res.status(HttpStatus.INTERNAL_SERVER_ERROR);
return of(unknownError);
}),
Expand Down
2 changes: 2 additions & 0 deletions backend/libs/logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { LoggerService } from './modules/logger/logger.service';
export { OpenobserveModule } from './modules/openobserve/openobserve.module';
6 changes: 6 additions & 0 deletions backend/libs/logger/src/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Log = {
level: 'log' | 'error' | 'warn' | 'http_exception';
payload: any;
serviceName: string;
time: Date;
};
59 changes: 59 additions & 0 deletions backend/libs/logger/src/modules/logger/logger.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common';
import { format } from 'date-fns';
import { OpenobserveService } from '../openobserve/openobserve.service';

const red = '\x1b[31m';
const yellow = '\x1b[33m';
const green = '\x1b[32m';
const reset = '\x1b[0m';

@Injectable()
export class LoggerService implements NestLoggerService {
constructor(private readonly openobserve: OpenobserveService) {}

log(message: any, serviceName1?: string, serviceName2?: string) {
this.logToOpenobserve('log', message, serviceName1, serviceName2);
}

error(message: any, serviceName1?: string, serviceName2?: string) {
this.logToOpenobserve('error', message, serviceName1, serviceName2);
}

warn(message: any, serviceName1?: string, serviceName2?: string) {
this.logToOpenobserve('warn', message, serviceName1, serviceName2);
}

private logToOpenobserve(
level: 'log' | 'error' | 'warn',
message: any,
serviceName1: string | undefined,
serviceName2: string | undefined,
) {
const serviceName = serviceName1 || serviceName2 || 'unknown';
const time = new Date();
this.openobserve
.log({
level,
payload: { message },
serviceName,
time,
})
.finally(() => this.coloredLog(level, serviceName, message, time));
}

private coloredLog(level: 'log' | 'error' | 'warn', serviceName: string, message: any, time: Date) {
const payload = typeof message === 'object' ? JSON.stringify(message) : String(message);

// eslint-disable-next-line no-console
console.log(
format(time, 'yyyy-MM-dd, HH:mm:ss.SSS'),
green,
`[${serviceName}]`,
level === 'error' ? red : level === 'warn' ? yellow : reset,
payload,
reset,
);
}
}
20 changes: 20 additions & 0 deletions backend/libs/logger/src/modules/openobserve/openobserve.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { ModuleMetadata, Provider, Type } from '@nestjs/common';

export interface OpenobserveModuleOptions {
endpoint: string;
key: string;
organizationId: string;
appId: string;
user: string;
}

export interface OpenobserveModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
name?: string;
useExisting?: Type<OpenobserveModuleOptions>;
useClass?: Type<OpenobserveModuleOptions>;
useFactory?: (...args: Array<any>) => Promise<OpenobserveModuleOptions> | OpenobserveModuleOptions;
inject?: Array<any>;
extraProviders?: Array<Provider>;
}
55 changes: 55 additions & 0 deletions backend/libs/logger/src/modules/openobserve/openobserve.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { DynamicModule, Module, Provider } from '@nestjs/common';
import { OpenobserveModuleAsyncOptions, OpenobserveModuleOptions } from './openobserve.model';
import { OpenobserveService } from './openobserve.service';

@Module({})
export class OpenobserveModule {
public static registerAsync(options: OpenobserveModuleAsyncOptions): DynamicModule {
const asyncProviders = this.createAsyncProviders(options);

return {
module: OpenobserveModule,
imports: options.imports || [],
providers: [...asyncProviders, OpenobserveService, ...(options.extraProviders || [])],
exports: [OpenobserveService],
};
}

private static createAsyncProviders(options: OpenobserveModuleAsyncOptions): Array<Provider> {
if (options.useFactory) {
return [
{
provide: 'OPENOBSERVE_CONFIG',
useFactory: options.useFactory,
inject: options.inject || [],
},
];
}

if (options.useClass) {
return [
{
provide: 'OPENOBSERVE_CONFIG',
useFactory: async (optionsFactory: OpenobserveModuleOptions) => optionsFactory,
inject: [options.useClass],
},
{
provide: options.useClass,
useClass: options.useClass,
},
];
}

if (options.useExisting) {
return [
{
provide: 'OPENOBSERVE_CONFIG',
useFactory: async (optionsFactory: OpenobserveModuleOptions) => optionsFactory,
inject: [options.useExisting],
},
];
}

throw new Error('Invalid async options');
}
}
Loading