From f5a9acbcaf379b2012ff47d50344a98123994739 Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Thu, 20 Jun 2024 20:50:26 +0700 Subject: [PATCH 1/9] chore(be): :fire: remove unused --- apps/be/common/src/utils/dtos/pagination-response.dto.ts | 8 -------- apps/be/src/app/task-logs/task-logs.service.ts | 2 -- apps/be/src/app/tasks/tasks.service.ts | 2 -- 3 files changed, 12 deletions(-) diff --git a/apps/be/common/src/utils/dtos/pagination-response.dto.ts b/apps/be/common/src/utils/dtos/pagination-response.dto.ts index 68b730d..ec1ced3 100644 --- a/apps/be/common/src/utils/dtos/pagination-response.dto.ts +++ b/apps/be/common/src/utils/dtos/pagination-response.dto.ts @@ -4,8 +4,6 @@ export class PaginationResponseDto { constructor(data: { data: TData[]; total: number; limit: number; page: number }) { this.data = data.data; this.total = data.total; - this.limit = data.limit; - this.page = data.page; } @ApiProperty() @@ -13,10 +11,4 @@ export class PaginationResponseDto { @ApiProperty() total: number; - - @ApiProperty() - limit: number; - - @ApiProperty() - page: number; } diff --git a/apps/be/src/app/task-logs/task-logs.service.ts b/apps/be/src/app/task-logs/task-logs.service.ts index 45ce216..bf72019 100644 --- a/apps/be/src/app/task-logs/task-logs.service.ts +++ b/apps/be/src/app/task-logs/task-logs.service.ts @@ -91,8 +91,6 @@ export class TaskLogsService { return { data: logs, total, - page: query.page || 1, - limit: query.limit || logs?.length || 0, }; } diff --git a/apps/be/src/app/tasks/tasks.service.ts b/apps/be/src/app/tasks/tasks.service.ts index d0a4f58..7d6405d 100644 --- a/apps/be/src/app/tasks/tasks.service.ts +++ b/apps/be/src/app/tasks/tasks.service.ts @@ -335,8 +335,6 @@ export class TasksService implements OnModuleInit { return { data: tasks, total, - page: query.page || 1, - limit: query.limit || tasks?.length, }; } From 50b6262f40388838194b7e2e32a777db8b8e60ea Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Fri, 21 Jun 2024 11:56:21 +0700 Subject: [PATCH 2/9] feat(be): :zap: enable redis keepAlive --- apps/be/common/src/redis/redis.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/be/common/src/redis/redis.module.ts b/apps/be/common/src/redis/redis.module.ts index e6084e0..ffaecfb 100644 --- a/apps/be/common/src/redis/redis.module.ts +++ b/apps/be/common/src/redis/redis.module.ts @@ -15,6 +15,7 @@ import { RedisService } from './services'; port: config.getOrThrow('REDIS_PORT'), password: config.getOrThrow('REDIS_PASSWORD'), connectTimeout: Number(config.get('REDIS_CONNECT_TIMEOUT')) || 20000, + keepAlive: Number(config.get('REDIS_KEEP_ALIVE')) || 1000, // Send a PING every 10 seconds maxRetriesPerRequest: null, reconnectOnError: () => { const reconnectAndResendFailedCmd = 2; From 532d3fa2b853893454e4f8a0cb9092a3489d2063 Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Fri, 21 Jun 2024 14:35:39 +0700 Subject: [PATCH 3/9] fix(fe): :bug: set default for switch --- apps/fe/src/components/form/task-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/fe/src/components/form/task-form.tsx b/apps/fe/src/components/form/task-form.tsx index f8665ee..0c388e1 100644 --- a/apps/fe/src/components/form/task-form.tsx +++ b/apps/fe/src/components/form/task-form.tsx @@ -60,6 +60,7 @@ export const TaskForm: React.FC = ({ mode, defaultValues, onSubmi }); useEffect(() => { + setValue('isEnable', false); if (defaultValues && Object.keys(defaultValues).length > 0) { const taskForm = new TaskFormValidator(); Object.entries(defaultValues).forEach(([key, value]) => { From 7b9c908dfd21e66df618db1d11698301b13c2953 Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Fri, 21 Jun 2024 14:36:05 +0700 Subject: [PATCH 4/9] fix(fe): :bug: missing default buttons on header form --- apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx | 5 +++-- apps/fe/src/app/(auth)/tasks/page.tsx | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx b/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx index 164aa7a..953b812 100644 --- a/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx +++ b/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx @@ -178,8 +178,9 @@ export default function LogList() { } canCreate={false} - headerButtons={ + headerButtons={({ defaultButtons }) => ( <> + {defaultButtons} invalidate({ @@ -189,7 +190,7 @@ export default function LogList() { } /> - } + )} > }> + ( + <> + {defaultButtons} + + + )} + > {...tableProps} rowKey="_id" From 62aa38edf984b3c9dbc4edd71e21ec028298958f Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Sat, 22 Jun 2024 00:38:20 +0700 Subject: [PATCH 5/9] feat(fe): :zap: move highlight code to component --- .../src/app/(auth)/tasks/show/[id]/page.tsx | 88 ++----------------- .../fe/src/components/show/highlight-code.tsx | 81 +++++++++++++++++ 2 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 apps/fe/src/components/show/highlight-code.tsx diff --git a/apps/fe/src/app/(auth)/tasks/show/[id]/page.tsx b/apps/fe/src/app/(auth)/tasks/show/[id]/page.tsx index 8673b65..ccc23bb 100644 --- a/apps/fe/src/app/(auth)/tasks/show/[id]/page.tsx +++ b/apps/fe/src/app/(auth)/tasks/show/[id]/page.tsx @@ -1,25 +1,18 @@ 'use client'; -import { useContext, useEffect, useState } from 'react'; -import { Descriptions, Typography, Spin, Button } from 'antd'; +import Link from 'next/link'; +import { Descriptions, Typography, Button } from 'antd'; import { ExportButton, Show } from '@refinedev/antd'; import { useExport, useShow } from '@refinedev/core'; -import { Highlight, themes } from 'prism-react-renderer'; -import { format } from 'prettier'; -import * as prettierBabel from 'prettier/plugins/babel'; -import * as prettierMd from 'prettier/plugins/markdown'; -import * as prettierEstree from 'prettier/plugins/estree'; +import { FileProtectOutlined } from '@ant-design/icons'; import { type TaskDto } from '~be/app/tasks/dtos'; -import { ColorModeContext } from '~/contexts/color-mode'; import { formatDateToHumanReadable } from '~/libs/utils/common'; -import Link from 'next/link'; -import { FileProtectOutlined } from '@ant-design/icons'; +import { HighlightCode } from '~/components/show'; const { Text } = Typography; export default function TaskShow() { - const { mode } = useContext(ColorModeContext); const { queryResult: { data: { data: record } = {}, isLoading }, } = useShow({}); @@ -32,33 +25,6 @@ export default function TaskShow() { }, }); - const [formattedHeaders, setFormattedHeaders] = useState(null); - const [formattedBody, setFormattedBody] = useState(null); - const [formatting, setFormatting] = useState(false); - - useEffect(() => { - const formatCode = async () => { - setFormatting(true); - if (record?.headers) { - const formatted = await format(record.headers, { - plugins: [prettierEstree, prettierBabel], - parser: 'json-stringify', - }); - setFormattedHeaders(formatted); - } - if (record?.body) { - const formatted = await format(record.body, { - plugins: [prettierEstree, prettierMd], - parser: 'markdown', - }); - setFormattedBody(formatted); - } - setFormatting(false); - }; - - formatCode(); - }, [record?.headers, record?.body]); - return ( - {formatting ? ( - - ) : ( - <> - - {({ className, style, tokens, getLineProps, getTokenProps }) => ( -
-                                        {tokens.map((line, i) => (
-                                            
- {line.map((token, key) => ( - - ))} -
- ))} -
- )} -
- - )} +
- {formatting ? ( - - ) : ( - - {({ className, style, tokens, getLineProps, getTokenProps }) => ( -
-                                    {tokens.map((line, i) => (
-                                        
- {line.map((token, key) => ( - - ))} -
- ))} -
- )} -
- )} +
diff --git a/apps/fe/src/components/show/highlight-code.tsx b/apps/fe/src/components/show/highlight-code.tsx new file mode 100644 index 0000000..bc96c17 --- /dev/null +++ b/apps/fe/src/components/show/highlight-code.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { useContext, useEffect, useState } from 'react'; +import { Skeleton } from 'antd'; +import { Highlight, themes } from 'prism-react-renderer'; +import { format } from 'prettier'; +import * as prettierBabel from 'prettier/plugins/babel'; +import * as prettierMd from 'prettier/plugins/markdown'; +import * as prettierEstree from 'prettier/plugins/estree'; +import * as prettierHtml from 'prettier/plugins/html'; + +import { ColorModeContext } from '~/contexts/color-mode'; + +export type HighlightCodeProps = { + source: string; + formatType: 'json' | 'markdown' | 'html'; +}; + +export function HighlightCode({ source = '', formatType }: HighlightCodeProps) { + const { mode } = useContext(ColorModeContext); + const [formattedSource, setFormattedSource] = useState(''); + const [isFormatting, setIsFormatting] = useState(false); + + useEffect(() => { + const formatSource = async () => { + setIsFormatting(true); + switch (formatType) { + case 'json': { + const formatted = await format(source, { + plugins: [prettierEstree, prettierBabel], + parser: 'json-stringify', + }); + setFormattedSource(formatted?.trim()); + break; + } + case 'html': { + const formatted = await format(source, { + plugins: [prettierEstree, prettierHtml], + parser: 'html', + }); + setFormattedSource(formatted?.trim()); + break; + } + case 'markdown': + default: { + const formatted = await format(source, { + plugins: [prettierEstree, prettierMd], + parser: 'markdown', + }); + setFormattedSource(formatted?.trim()); + break; + } + } + setIsFormatting(false); + }; + + formatSource(); + }, [source, formatType]); + + return isFormatting ? ( + + ) : ( + + {({ style, tokens, getLineProps, getTokenProps }) => ( +
+                    {tokens.map((line, i) => (
+                        
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+ )} +
+ ); +} From 4d92128a14aea770bb32d51afb00955a9dccea0f Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Sat, 22 Jun 2024 02:19:03 +0700 Subject: [PATCH 6/9] feat(fe): add auto language detection --- .../fe/src/components/show/highlight-code.tsx | 86 ++++++++++++------- apps/fe/src/components/show/index.ts | 1 + 2 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 apps/fe/src/components/show/index.ts diff --git a/apps/fe/src/components/show/highlight-code.tsx b/apps/fe/src/components/show/highlight-code.tsx index bc96c17..6862c3d 100644 --- a/apps/fe/src/components/show/highlight-code.tsx +++ b/apps/fe/src/components/show/highlight-code.tsx @@ -13,57 +13,85 @@ import { ColorModeContext } from '~/contexts/color-mode'; export type HighlightCodeProps = { source: string; - formatType: 'json' | 'markdown' | 'html'; + formatType: 'json' | 'markdown' | 'html' | 'xml' | 'auto'; }; export function HighlightCode({ source = '', formatType }: HighlightCodeProps) { const { mode } = useContext(ColorModeContext); const [formattedSource, setFormattedSource] = useState(''); const [isFormatting, setIsFormatting] = useState(false); + const [language, setLanguage] = useState(formatType); useEffect(() => { const formatSource = async () => { setIsFormatting(true); - switch (formatType) { - case 'json': { - const formatted = await format(source, { - plugins: [prettierEstree, prettierBabel], - parser: 'json-stringify', - }); - setFormattedSource(formatted?.trim()); - break; - } - case 'html': { - const formatted = await format(source, { - plugins: [prettierEstree, prettierHtml], - parser: 'html', - }); - setFormattedSource(formatted?.trim()); - break; - } - case 'markdown': - default: { - const formatted = await format(source, { - plugins: [prettierEstree, prettierMd], - parser: 'markdown', - }); - setFormattedSource(formatted?.trim()); - break; + const trimmedSource = source?.trim(); + try { + switch (language) { + case 'auto': { + if (//i.test(source)) { + setLanguage('html'); + } else if (/^<\?xml/i.test(source)) { + setLanguage('xml'); + } else if (/^\s*\{/.test(source)) { + setLanguage('json'); + } else { + // Fallback to markdown + throw new Error('Non-auto language detected. Fallback to markdown.'); + } + break; + } + case 'json': { + const formatted = await format(trimmedSource, { + plugins: [prettierEstree, prettierBabel], + parser: 'json-stringify', + }); + setFormattedSource(formatted?.trim()); + break; + } + case 'html': { + const formatted = await format(trimmedSource, { + plugins: [prettierEstree, prettierHtml], + parser: 'html', + }); + setFormattedSource(formatted?.trim()); + break; + } + case 'xml': { + const formatted = await format(trimmedSource, { + plugins: [prettierEstree, prettierHtml], + parser: 'xml', + }); + setFormattedSource(formatted?.trim()); + break; + } + case 'markdown': + default: { + const formatted = await format(trimmedSource, { + plugins: [prettierEstree, prettierMd], + parser: 'markdown', + }); + setFormattedSource(formatted?.trim()); + break; + } } + } catch { + setLanguage('markdown'); } + setIsFormatting(false); }; formatSource(); - }, [source, formatType]); + }, [source, language]); return isFormatting ? ( ) : ( {({ style, tokens, getLineProps, getTokenProps }) => (
diff --git a/apps/fe/src/components/show/index.ts b/apps/fe/src/components/show/index.ts
new file mode 100644
index 0000000..3800d18
--- /dev/null
+++ b/apps/fe/src/components/show/index.ts
@@ -0,0 +1 @@
+export * from './highlight-code';

From 61a779ef3ec73923d246a9de4499ce46163baf31 Mon Sep 17 00:00:00 2001
From: lehuygiang28 
Date: Sat, 22 Jun 2024 02:21:41 +0700
Subject: [PATCH 7/9] feat: :sparkles: update request/response in task logs

---
 apps/be/common/src/axios/index.ts             | 12 ++-
 .../src/utils/abstract/abstract.repository.ts | 30 +++++-
 apps/be/common/src/utils/common.ts            | 23 +++++
 .../be/src/app/task-logs/dtos/task-log.dto.ts |  8 +-
 apps/be/src/app/task-logs/task-log.schema.ts  | 20 ++++
 .../app/tasks/processors/task.processor.ts    | 31 ++++++-
 .../src/app/(auth)/tasks/logs/[id]/page.tsx   | 93 +++++++++++++++++--
 apps/fe/src/libs/utils/common.ts              |  7 ++
 8 files changed, 203 insertions(+), 21 deletions(-)

diff --git a/apps/be/common/src/axios/index.ts b/apps/be/common/src/axios/index.ts
index 8243eb3..c6d5818 100644
--- a/apps/be/common/src/axios/index.ts
+++ b/apps/be/common/src/axios/index.ts
@@ -56,12 +56,14 @@ export const httpTimerAdapter = async (
     });
 };
 
+export const defaultHeaders = {
+    'Content-Type': 'application/json, text/plain, */*',
+    'User-Agent':
+        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36',
+};
+
 export const axiosConfig: HttpModuleOptions = {
-    headers: {
-        'Content-Type': 'application/text',
-        'User-Agent':
-            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36',
-    },
+    headers: defaultHeaders,
     httpAgent: new http.Agent({ keepAlive: true }),
     httpsAgent: new https.Agent({ keepAlive: true }),
     adapter: httpTimerAdapter,
diff --git a/apps/be/common/src/utils/abstract/abstract.repository.ts b/apps/be/common/src/utils/abstract/abstract.repository.ts
index bfad054..eca2d8b 100644
--- a/apps/be/common/src/utils/abstract/abstract.repository.ts
+++ b/apps/be/common/src/utils/abstract/abstract.repository.ts
@@ -15,6 +15,7 @@ import {
 } from 'mongoose';
 import { NotFoundException } from '@nestjs/common';
 import type { NullableType } from '../types';
+import { PaginationRequestDto } from '../dtos';
 
 export abstract class AbstractRepository {
     protected abstract readonly logger: PinoLogger;
@@ -143,36 +144,61 @@ export abstract class AbstractRepository {
         });
     }
 
+    /**
+     * Pagination will automatically calculate, if query is provided
+     * @returns
+     */
     async find({
         filterQuery,
         queryOptions,
         projectionType,
+        query,
     }: {
         filterQuery: FilterQuery;
         queryOptions?: Partial>;
         projectionType?: ProjectionType;
+        query?: PaginationRequestDto;
     }): Promise> {
-        const document = await this.model.find(filterQuery, projectionType, {
+        const filter: FilterQuery = { ...filterQuery };
+        const options: Partial> = {
             lean: true,
             ...queryOptions,
-        });
+        };
+
+        if (query?.sortBy && query?.sortOrder) {
+            options.sort = { [query.sortBy]: query.sortOrder };
+        }
+
+        if (query?.page && query?.limit) {
+            options.skip = (query.page - 1) * query.limit;
+            options.limit = query.limit;
+        }
+
+        const document = await this.model.find(filter, projectionType, options);
 
         return document ?? null;
     }
 
+    /**
+     * Pagination will automatically calculate, if query is provided
+     * @returns
+     */
     async findOrThrow({
         filterQuery,
         queryOptions,
         projectionType,
+        query,
     }: {
         filterQuery: FilterQuery;
         queryOptions?: Partial>;
         projectionType?: ProjectionType;
+        query?: PaginationRequestDto;
     }): Promise {
         const document = await this.find({
             filterQuery,
             queryOptions,
             projectionType,
+            query,
         });
 
         if (!document || document.length <= 0) {
diff --git a/apps/be/common/src/utils/common.ts b/apps/be/common/src/utils/common.ts
index ef5d7c1..136b179 100644
--- a/apps/be/common/src/utils/common.ts
+++ b/apps/be/common/src/utils/common.ts
@@ -67,3 +67,26 @@ export function valuesOfEnum(enumObj: T): (string | number)[]
 
     return values;
 }
+
+/**
+ * Normalizes headers by converting all keys to lowercase and trimming whitespace.
+ * @param headers
+ * @returns
+ */
+export function normalizeHeaders(headers: Record) {
+    if (typeof headers !== 'object' || headers === null || headers === undefined) {
+        throw new Error('Headers must be an object');
+    }
+
+    return Object.entries(headers).reduce((acc, [key, value]) => {
+        if (typeof key !== 'string') {
+            throw new Error('Header keys must be strings');
+        }
+
+        if (value === undefined || value === null) {
+            return acc;
+        }
+
+        return { ...acc, [key.trim().toLowerCase()]: value };
+    }, {});
+}
diff --git a/apps/be/src/app/task-logs/dtos/task-log.dto.ts b/apps/be/src/app/task-logs/dtos/task-log.dto.ts
index 841f56f..667dba0 100644
--- a/apps/be/src/app/task-logs/dtos/task-log.dto.ts
+++ b/apps/be/src/app/task-logs/dtos/task-log.dto.ts
@@ -1,7 +1,7 @@
 import { Types } from 'mongoose';
 import { ApiProperty } from '@nestjs/swagger';
 import { IsDate, IsNumber, IsOptional, IsString } from 'class-validator';
-import { LogTimingPhases, TaskLog } from '../task-log.schema';
+import { LogTimingPhases, RequestObject, ResponseObject, TaskLog } from '../task-log.schema';
 
 export class LogTimingPhasesDto implements LogTimingPhases {
     @ApiProperty({ type: Number, required: false, description: 'The time spent waiting' })
@@ -111,6 +111,12 @@ export class TaskLogDto implements TaskLog {
     @ApiProperty({ type: LogTimingPhasesDto, required: false })
     timings?: LogTimingPhasesDto;
 
+    @ApiProperty({ type: RequestObject, required: false })
+    request?: RequestObject;
+
+    @ApiProperty({ type: ResponseObject, required: false })
+    response?: ResponseObject;
+
     @ApiProperty({ type: Date, required: false })
     @IsDate()
     @IsOptional()
diff --git a/apps/be/src/app/task-logs/task-log.schema.ts b/apps/be/src/app/task-logs/task-log.schema.ts
index df4f58d..fd0ea23 100644
--- a/apps/be/src/app/task-logs/task-log.schema.ts
+++ b/apps/be/src/app/task-logs/task-log.schema.ts
@@ -1,4 +1,5 @@
 import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
+import { IntersectionType, PickType } from '@nestjs/swagger';
 import { type Timings } from '@szmarczak/http-timer';
 import { HydratedDocument, Types } from 'mongoose';
 
@@ -32,6 +33,19 @@ export class LogTimingPhases implements Phases {
     total?: number;
 }
 
+export class RequestObject extends Object {
+    @Prop({ required: false, type: Object })
+    headers?: Record;
+
+    @Prop({ required: false, type: String, default: '' })
+    body?: string;
+}
+
+export class ResponseObject extends IntersectionType(
+    Object,
+    PickType(RequestObject, ['headers', 'body']),
+) {}
+
 @Schema({
     timestamps: true,
     collection: 'taskLogs',
@@ -67,6 +81,12 @@ export class TaskLog extends AbstractDocument {
     @Prop({ required: false, type: LogTimingPhases })
     timings?: LogTimingPhases;
 
+    @Prop({ required: false, type: RequestObject })
+    request?: RequestObject;
+
+    @Prop({ required: false, type: ResponseObject })
+    response?: ResponseObject;
+
     @Prop({ required: false, default: new Date() })
     createdAt?: Date;
 
diff --git a/apps/be/src/app/tasks/processors/task.processor.ts b/apps/be/src/app/tasks/processors/task.processor.ts
index 438e9d1..efeb04d 100644
--- a/apps/be/src/app/tasks/processors/task.processor.ts
+++ b/apps/be/src/app/tasks/processors/task.processor.ts
@@ -8,6 +8,8 @@ import { BULLMQ_TASK_LOG_QUEUE, BULLMQ_TASK_QUEUE } from '~be/common/bullmq';
 import { Task } from '~be/app/tasks/schemas/task.schema';
 import { CreateTaskLogDto, TaskLogsJobName } from '~be/app/task-logs';
 import { type Timings } from '@szmarczak/http-timer';
+import { defaultHeaders } from '~be/common/axios';
+import { normalizeHeaders } from '~be/common/utils';
 
 @Injectable()
 @Processor(BULLMQ_TASK_QUEUE, {
@@ -45,10 +47,14 @@ export class TaskProcessor extends WorkerHost implements OnModuleInit {
         const now = Date.now();
         const { name, endpoint, method, body, headers } = job.data;
 
+        const normalizedHeaders = headers ? normalizeHeaders(JSON.parse(headers)) : {};
+
+        const headersValidated = Object.assign(normalizeHeaders(defaultHeaders), normalizedHeaders);
+
         const config: AxiosRequestConfig = {
             url: endpoint,
             method,
-            headers: headers ? JSON.parse(headers) : undefined,
+            headers: headersValidated,
             data: body,
         };
 
@@ -59,7 +65,7 @@ export class TaskProcessor extends WorkerHost implements OnModuleInit {
             timings = response.request['timings'] || null;
 
             this.logger.info(
-                `FETCH ${name} - ${response?.status} - ${timings?.phases?.total} ms - ${JSON.stringify(response?.data)?.length ?? 0} bytes`,
+                `FETCH ${name} - ${response?.status} - ${timings?.phases?.total} ms - ${String(response?.data ?? '')?.length ?? 0} bytes`,
             );
         } catch (error: AxiosError | unknown) {
             if (error instanceof AxiosError) {
@@ -71,6 +77,8 @@ export class TaskProcessor extends WorkerHost implements OnModuleInit {
             timings = null;
         }
 
+        const stringBody = String(response?.data ?? '');
+
         const taskLog: CreateTaskLogDto = {
             taskId: job.data._id,
             endpoint,
@@ -81,13 +89,28 @@ export class TaskProcessor extends WorkerHost implements OnModuleInit {
 
             duration: timings?.phases?.total ?? 0,
             statusCode: response?.status ?? 0,
-            responseSizeBytes: JSON.stringify(response?.data)?.length ?? 0,
+            responseSizeBytes: stringBody?.length ?? 0,
             timings: timings?.phases || {},
+
+            request: {
+                headers: headersValidated,
+                body: String(config?.data || ''),
+            },
+
+            response: {
+                headers: response?.headers,
+                body:
+                    stringBody?.length > Number(process.env['MAX_BODY_LOG_SIZE'] || 1024 * 50) // Default 50KB
+                        ? `Body too large (${stringBody?.length} bytes), will not be logged.`
+                        : stringBody,
+            },
         };
 
         await this.taskLogQueue.add(`saveTaskLog`, taskLog, {
             removeOnComplete: 1,
-            attempts: 5,
+            removeOnFail: 1,
+            attempts: 10,
+            // delay: 5000,
             backoff: {
                 type: 'exponential',
                 delay: 5000,
diff --git a/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx b/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx
index 953b812..957ec45 100644
--- a/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx
+++ b/apps/fe/src/app/(auth)/tasks/logs/[id]/page.tsx
@@ -3,7 +3,7 @@
 import Link from 'next/link';
 import { HttpError, useParsed, useInvalidate } from '@refinedev/core';
 import { List, ShowButton, useTable, RefreshButton } from '@refinedev/antd';
-import { Breadcrumb, Space, Table, Modal, Typography, Descriptions } from 'antd';
+import { Breadcrumb, Space, Table, Modal, Typography, Descriptions, Collapse } from 'antd';
 import { format } from 'date-fns';
 import { useState } from 'react';
 import {
@@ -23,9 +23,15 @@ import { Line, Bar } from 'react-chartjs-2';
 import { HttpMethodTag } from '~/components/tag/http-method-tag';
 import { HttpMethodEnum } from '~be/app/tasks/tasks.enum';
 import { type TaskLogDto } from '~be/app/task-logs';
-import { formatDateToHumanReadable, getJitter, sortArrayByKey } from '~/libs/utils/common';
+import {
+    camelCaseToCapitalizedWords,
+    formatDateToHumanReadable,
+    getJitter,
+    sortArrayByKey,
+} from '~/libs/utils/common';
 import { HttpStatusTag } from '~/components/tag/http-status-tag';
 import { HttpStatus } from '@nestjs/common';
+import { HighlightCode } from '~/components/show';
 
 const { Title: TextTitle, Text } = Typography;
 const { Item: DesItem } = Descriptions;
@@ -363,7 +369,7 @@ export default function LogList() {
                     
Task Log Details} > @@ -375,15 +381,75 @@ export default function LogList() { {selectedLog.endpoint} - {selectedLog.method} - {selectedLog.statusCode} + + + + + + {selectedLog.workerName} + + + ), + }, + { + key: 'request-body', + label: 'Body', + children: ( + + ), + }, + ]} + /> + + + + ), + }, + { + key: 'response-body', + label: 'Body', + children: ( + + ), + }, + ]} + /> + Timings} > @@ -394,14 +460,23 @@ export default function LogList() { {formatDateToHumanReadable(selectedLog.scheduledAt)} - - {selectedLog.timings?.total} ms - {(selectedLog.responseSizeBytes / 1024).toFixed(2)} KB + + {selectedLog.timings && + Object.entries(selectedLog.timings).map(([key, value]) => { + return ( + + {value} ms + + ); + })} {selectedLog.timings && ( diff --git a/apps/fe/src/libs/utils/common.ts b/apps/fe/src/libs/utils/common.ts index 57441d8..28c60d3 100644 --- a/apps/fe/src/libs/utils/common.ts +++ b/apps/fe/src/libs/utils/common.ts @@ -47,3 +47,10 @@ export function getJitter(taskLog: TaskLogDto) { (new Date(taskLog.executedAt).getTime() - new Date(taskLog.scheduledAt).getTime()) / 1000; return diff; } + +export function camelCaseToCapitalizedWords(str: string) { + return str + .replace(/([A-Z])/g, ' $1') + .replace(/^./, (s) => s.toUpperCase()) + .trim(); +} From 56aa4418a96103f15233328eb99b67d572cea148 Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Sat, 22 Jun 2024 02:27:14 +0700 Subject: [PATCH 8/9] chore(be): update redis keepAlive to 10 seconds --- apps/be/common/src/redis/redis.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/be/common/src/redis/redis.module.ts b/apps/be/common/src/redis/redis.module.ts index ffaecfb..9e814d5 100644 --- a/apps/be/common/src/redis/redis.module.ts +++ b/apps/be/common/src/redis/redis.module.ts @@ -15,7 +15,7 @@ import { RedisService } from './services'; port: config.getOrThrow('REDIS_PORT'), password: config.getOrThrow('REDIS_PASSWORD'), connectTimeout: Number(config.get('REDIS_CONNECT_TIMEOUT')) || 20000, - keepAlive: Number(config.get('REDIS_KEEP_ALIVE')) || 1000, // Send a PING every 10 seconds + keepAlive: Number(config.get('REDIS_KEEP_ALIVE')) || 10000, // Send a PING every 10 seconds maxRetriesPerRequest: null, reconnectOnError: () => { const reconnectAndResendFailedCmd = 2; From b752ab3977e9ae3fe7ce88c0b0ec8e8eaf48157e Mon Sep 17 00:00:00 2001 From: lehuygiang28 Date: Sat, 22 Jun 2024 02:40:14 +0700 Subject: [PATCH 9/9] refactor(fe): optimize code highlighting in show component --- apps/fe/src/components/show/highlight-code.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/fe/src/components/show/highlight-code.tsx b/apps/fe/src/components/show/highlight-code.tsx index 6862c3d..d146924 100644 --- a/apps/fe/src/components/show/highlight-code.tsx +++ b/apps/fe/src/components/show/highlight-code.tsx @@ -96,9 +96,9 @@ export function HighlightCode({ source = '', formatType }: HighlightCodeProps) { {({ style, tokens, getLineProps, getTokenProps }) => (
                     {tokens.map((line, i) => (
-                        
+
{line.map((token, key) => ( - + ))}
))}