Skip to content

Commit

Permalink
feat(be): ✨ add bull-board to visualize bullmq queues
Browse files Browse the repository at this point in the history
[docker-latest]
  • Loading branch information
lehuygiang28 committed Jun 30, 2024
1 parent 2324fd8 commit a5da8eb
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 13 deletions.
21 changes: 19 additions & 2 deletions apps/be/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join } from 'node:path';
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule, ConfigService, ConditionalModule } from '@nestjs/config';
import { BullModule } from '@nestjs/bullmq';
import { ServeStaticModule } from '@nestjs/serve-static';
Expand All @@ -18,6 +18,8 @@ import { UsersModule } from './users/users.module';
import { TasksModule } from './tasks/tasks.module';
import { StatsModule } from './stats/stats.module';
import { AppConfigService } from './config/app-config.service';
import { jobQueueUIMiddleware } from './middlewares';
import { AllConfig } from './config';

@Module({
imports: [
Expand Down Expand Up @@ -58,4 +60,19 @@ import { AppConfigService } from './config/app-config.service';
providers: [AppService, AppConfigService],
exports: [AppConfigService],
})
export class AppModule {}
export class AppModule {
constructor(
private readonly configService: ConfigService<AllConfig>,
private readonly redisService: RedisService,
) {}
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
jobQueueUIMiddleware(
this.redisService.getClient,
this.configService.get('app.globalPrefix', { infer: true }) ?? '',
),
)
.forRoutes('/admin/queues');
}
}
7 changes: 7 additions & 0 deletions apps/be/src/app/config/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class EnvironmentVariablesValidator {
@IsString()
WORKER_NAME: string;

@IsOptional()
@IsString()
GLOBAL_PREFIX: string;

@IsOptional()
@IsNumber()
@Type(() => Number)
Expand Down Expand Up @@ -53,6 +57,9 @@ export default registerAs<AppConfig>('app', () => {
return {
deployEnv: process.env?.DEPLOY_ENV || 'none',
workerMode: process.env?.WORKER_MODE === 'true',
globalPrefix: process.env?.GLOBAL_PREFIX
? process.env.GLOBAL_PREFIX.replace(/^\/|\\|\/$|\\$/g, '') // remove leading and trailing slashes (/ or \)
: 'api',
workerName: process.env?.WORKER_NAME || 'default',
eventsMaxLen: process.env?.BULLMQ_EVENTS_MAXLEN
? parseInt(process.env?.BULLMQ_EVENTS_MAXLEN, 10)
Expand Down
1 change: 1 addition & 0 deletions apps/be/src/app/config/app-config.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type AppConfig = {
deployEnv: string;
workerMode: boolean;
workerName: string;
globalPrefix: string;
eventsMaxLen: number;
port: number;
fallbackLanguage: string;
Expand Down
1 change: 1 addition & 0 deletions apps/be/src/app/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './job-queues.middleware';
41 changes: 41 additions & 0 deletions apps/be/src/app/middlewares/job-queues.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { Queue } from 'bullmq';
import { Redis } from 'ioredis';
import {
BULLMQ_TASK_QUEUE,
BULLMQ_TASK_LOG_QUEUE,
BULLMQ_CLEAR_TASK_QUEUE,
BULLMQ_BG_JOB_QUEUE,
} from '~be/common/bullmq/bullmq.constant';

export function jobQueueUIMiddleware(connection: Redis, globalPrefix?: string) {
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath(`${globalPrefix ?? globalPrefix + '/'}/admin/queues`);

const queues = [
BULLMQ_TASK_QUEUE,
BULLMQ_TASK_LOG_QUEUE,
BULLMQ_CLEAR_TASK_QUEUE,
BULLMQ_BG_JOB_QUEUE,
].map(
(queueName) =>
new BullMQAdapter(
new Queue(queueName, {
connection,
}),
{
readOnlyMode: true,
description: 'Task Queue',
},
),
);

createBullBoard({
queues,
serverAdapter,
});

return serverAdapter.getRouter();
}
20 changes: 11 additions & 9 deletions apps/be/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ import { Logger } from 'nestjs-pino';
import { SwaggerTheme, SwaggerThemeNameEnum } from 'swagger-themes';
import * as swaggerStats from 'swagger-stats';

import { AppModule } from './app/app.module';
import {
ProblemDetails,
ProblemDetailsFilter,
ResolvePromisesInterceptor,
validationOptions,
} from '~be/common/utils';
import { AppModule } from './app/app.module';
import { AllConfig } from './app/config/all-config.type';

async function bootstrap() {
const port = process.env.PORT || 8000;

const app = await NestFactory.create(AppModule, { bufferLogs: true });
const configService = app.get(ConfigService);
const configService: ConfigService<AllConfig> = app.get(ConfigService);
const logger = app.get(Logger);

app.enableCors();
Expand All @@ -43,8 +44,7 @@ async function bootstrap() {
new ClassSerializerInterceptor(app.get(Reflector)),
);

const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
app.setGlobalPrefix(configService.getOrThrow('app.globalPrefix', { infer: true }));

const swaggerDocumentConfig = new DocumentBuilder()
.setTitle('Tasktr RESTful API Documentations')
Expand All @@ -70,18 +70,18 @@ async function bootstrap() {
};
SwaggerModule.setup('docs', app, document, swaggerCustomOptions);

if (configService.get('API_STATS_PATH')) {
if (configService.get('app.apiStatsPath', { infer: true })) {
app.use(
swaggerStats.getMiddleware({
uriPath: configService.getOrThrow<string>('API_STATS_PATH'),
uriPath: configService.getOrThrow('app.apiStatsPath', { infer: true }),
swaggerSpec: document,
name: 'Tasktr API statistics',
timelineBucketDuration: 180000,
authentication: true,
async onAuthenticate(req, username, password) {
if (
username === configService.getOrThrow<string>('API_STATS_USERNAME') &&
password === configService.getOrThrow<string>('API_STATS_PASSWORD')
username === configService.get('app.apiStatsUsername', { infer: true }) &&
password === configService.get('app.apiStatsPassword', { infer: true })
) {
return true;
}
Expand All @@ -93,7 +93,9 @@ async function bootstrap() {
}

await app.listen(port);
logger.log(`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`);
logger.log(
`🚀 Application is running on: http://localhost:${port}/${configService.get('app.globalPrefix', { infer: true })}`,
);
}

bootstrap();
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@ant-design/nextjs-registry": "^1.0.0",
"@bull-board/api": "^5.20.5",
"@bull-board/express": "^5.20.5",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.4.2",
"@kovalenko/is-cron": "^1.0.10",
Expand Down
47 changes: 45 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,38 @@ __metadata:
languageName: node
linkType: hard

"@bull-board/api@npm:5.20.5, @bull-board/api@npm:^5.20.5":
version: 5.20.5
resolution: "@bull-board/api@npm:5.20.5"
dependencies:
redis-info: "npm:^3.0.8"
peerDependencies:
"@bull-board/ui": 5.20.5
checksum: 10c0/0d4650c07e381d829bd2007be1dcfff21ec909b82da5a6db2f572c944f20aab8cf5522b70c29843dde8ffcdb4d7d0cd200631ea08df63ad74ec4b67865f142df
languageName: node
linkType: hard

"@bull-board/express@npm:^5.20.5":
version: 5.20.5
resolution: "@bull-board/express@npm:5.20.5"
dependencies:
"@bull-board/api": "npm:5.20.5"
"@bull-board/ui": "npm:5.20.5"
ejs: "npm:^3.1.10"
express: "npm:^4.19.2"
checksum: 10c0/baafa5ce860b7e6515990654359197869c495f36eb2741032a1c0ff0880422b0f65ef4c8285eafcbbf1f2b2868cc15e53d574b718e143ca78ce78f1e1a92bfeb
languageName: node
linkType: hard

"@bull-board/ui@npm:5.20.5":
version: 5.20.5
resolution: "@bull-board/ui@npm:5.20.5"
dependencies:
"@bull-board/api": "npm:5.20.5"
checksum: 10c0/5008ef0e925580a7576f2c143fcd4fc580af1b4a45b2b596fbcfd1321bcb947d3ab1f550c6fb5a25ceeb7aa43a2e312efe1b44959224a328711f70ffeb77d4cc
languageName: node
linkType: hard

"@colors/colors@npm:1.5.0":
version: 1.5.0
resolution: "@colors/colors@npm:1.5.0"
Expand Down Expand Up @@ -5032,6 +5064,8 @@ __metadata:
"@ant-design/icons": "npm:^5.3.7"
"@ant-design/nextjs-registry": "npm:^1.0.0"
"@babel/core": "npm:^7.24.6"
"@bull-board/api": "npm:^5.20.5"
"@bull-board/express": "npm:^5.20.5"
"@compodoc/compodoc": "npm:^1.1.25"
"@faker-js/faker": "npm:^8.4.1"
"@hookform/resolvers": "npm:^3.4.2"
Expand Down Expand Up @@ -10688,7 +10722,7 @@ __metadata:
languageName: node
linkType: hard

"express@npm:4.19.2, express@npm:^4.17.3, express@npm:^4.18.2":
"express@npm:4.19.2, express@npm:^4.17.3, express@npm:^4.18.2, express@npm:^4.19.2":
version: 4.19.2
resolution: "express@npm:4.19.2"
dependencies:
Expand Down Expand Up @@ -14455,7 +14489,7 @@ __metadata:
languageName: node
linkType: hard

"lodash@npm:4.17.21, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
"lodash@npm:4.17.21, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
Expand Down Expand Up @@ -19349,6 +19383,15 @@ __metadata:
languageName: node
linkType: hard

"redis-info@npm:^3.0.8":
version: 3.1.0
resolution: "redis-info@npm:3.1.0"
dependencies:
lodash: "npm:^4.17.11"
checksum: 10c0/ec0f31d97893c5828cec7166486d74198c92160c60073b6f2fe805cdf575a10ddcccc7641737d44b8f451355f0ab5b6c7b0d79e8fc24742b75dd625f91ffee38
languageName: node
linkType: hard

"redis-parser@npm:^3.0.0":
version: 3.0.0
resolution: "redis-parser@npm:3.0.0"
Expand Down

0 comments on commit a5da8eb

Please sign in to comment.