Skip to content

Commit 8ba6614

Browse files
committed
feat: support get scf logs by cls
1 parent 6305b3e commit 8ba6614

File tree

10 files changed

+413
-23
lines changed

10 files changed

+413
-23
lines changed

__tests__/cls.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ClsDeployInputs, ClsDeployOutputs } from './../src/modules/cls/interface';
2+
import { Scf } from '../src';
23
import { Cls } from '../src';
34
import { sleep } from '@ygkit/request';
45

@@ -7,6 +8,7 @@ describe('Cls', () => {
78
SecretId: process.env.TENCENT_SECRET_ID,
89
SecretKey: process.env.TENCENT_SECRET_KEY,
910
};
11+
const scf = new Scf(credentials, process.env.REGION);
1012
const client = new Cls(credentials, process.env.REGION);
1113

1214
let outputs: ClsDeployOutputs;
@@ -43,16 +45,37 @@ describe('Cls', () => {
4345
outputs = res;
4446
});
4547

46-
test('should remove cls success', async () => {
48+
test('remove cls', async () => {
4749
await sleep(5000);
4850
await client.remove(outputs);
4951

50-
const detail = await client.cls.getLogset({
51-
logset_id: outputs.logsetId,
52+
const detail = await client.cls.getTopic({
53+
topic_id: outputs.topicId,
5254
});
53-
expect(detail.logset_id).toBeUndefined();
55+
56+
expect(detail.topicId).toBeUndefined();
5457
expect(detail.error).toEqual({
5558
message: expect.any(String),
5659
});
5760
});
61+
62+
test('search log', async () => {
63+
await scf.invoke({
64+
namespace: 'default',
65+
functionName: 'serverless-unit-test',
66+
});
67+
68+
await sleep(5000);
69+
70+
const res = await client.getLogList({
71+
functionName: 'serverless-unit-test',
72+
namespace: 'default',
73+
qualifier: '$LATEST',
74+
logsetId: '125d5cd7-caee-49ab-af9b-da29aa09d6ab',
75+
topicId: 'e9e38c86-c7ba-475b-a852-6305880d2212',
76+
interval: 3600,
77+
});
78+
console.log('logs', res);
79+
expect(res).toBeInstanceOf(Array);
80+
});
5881
});

__tests__/scf.test.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -537,17 +537,6 @@ describe('Scf', () => {
537537
},
538538
]);
539539
});
540-
test('[remove cls] update', async () => {
541-
await sleep(3000);
542-
inputs.cls = {
543-
logsetId: '',
544-
topicId: '',
545-
};
546-
outputs = await scf.deploy(inputs);
547-
548-
expect(outputs.ClsLogsetId).toBe('');
549-
expect(outputs.ClsTopicId).toBe('');
550-
});
551540
test('invoke', async () => {
552541
const res = await scf.invoke({
553542
namespace: inputs.namespace,
@@ -567,6 +556,26 @@ describe('Scf', () => {
567556
RequestId: expect.any(String),
568557
});
569558
});
559+
test('get function logs', async () => {
560+
const logs = await scf.logs({
561+
functionName: inputs.name,
562+
namespace: inputs.namespace,
563+
});
564+
565+
expect(logs).toBeInstanceOf(Array);
566+
});
567+
test('[remove cls] update', async () => {
568+
await sleep(3000);
569+
inputs.cls = {
570+
logsetId: '',
571+
topicId: '',
572+
};
573+
outputs = await scf.deploy(inputs);
574+
575+
expect(outputs.ClsLogsetId).toBe('');
576+
expect(outputs.ClsTopicId).toBe('');
577+
});
578+
570579
test('remove', async () => {
571580
const res = await scf.remove({
572581
functionName: inputs.name,

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@
8383
},
8484
"dependencies": {
8585
"@tencent-sdk/capi": "^1.1.8",
86-
"@tencent-sdk/cls": "^0.1.7",
86+
"@tencent-sdk/cls": "^0.1.13",
8787
"@types/jest": "^26.0.20",
8888
"@types/node": "^14.14.31",
8989
"@ygkit/request": "^0.1.8",
9090
"cos-nodejs-sdk-v5": "2.8.6",
91+
"dayjs": "^1.10.4",
9192
"moment": "^2.29.1",
9293
"tencent-cloud-sdk": "^1.0.5",
9394
"type-fest": "^0.20.2"

src/modules/cls/index.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import { CapiCredentials, RegionType } from './../interface';
21
import { Cls as ClsClient } from '@tencent-sdk/cls';
2+
import dayjs, { Dayjs } from 'dayjs';
33
import {
44
ClsDelopyIndexInputs,
55
ClsDeployInputs,
66
ClsDeployLogsetInputs,
77
ClsDeployOutputs,
88
ClsDeployTopicInputs,
9+
GetLogOptions,
10+
GetLogDetailOptions,
11+
LogContent,
912
} from './interface';
13+
import { CapiCredentials, RegionType } from './../interface';
1014
import { ApiError } from '../../utils/error';
11-
import { createLogset, createTopic, updateIndex } from './utils';
15+
import { createLogset, createTopic, updateIndex, getSearchSql } from './utils';
16+
17+
const TimeFormat = 'YYYY-MM-DD HH:mm:ss';
1218

1319
export default class Cls {
1420
credentials: CapiCredentials;
@@ -192,4 +198,96 @@ export default class Cls {
192198

193199
return {};
194200
}
201+
202+
async getLogList(data: GetLogOptions) {
203+
const clsClient = new ClsClient({
204+
region: this.region,
205+
secretId: this.credentials.SecretId!,
206+
secretKey: this.credentials.SecretKey!,
207+
token: this.credentials.Token,
208+
debug: false,
209+
});
210+
211+
const { endTime, interval = 3600 } = data;
212+
let startDate: Dayjs;
213+
let endDate: Dayjs;
214+
215+
// 默认获取从当前到一个小时前时间段的日志
216+
if (!endTime) {
217+
endDate = dayjs();
218+
startDate = endDate.add(-1, 'hour');
219+
} else {
220+
endDate = dayjs(endTime);
221+
startDate = dayjs(endDate.valueOf() - Number(interval) * 1000);
222+
}
223+
224+
const sql = getSearchSql({
225+
...data,
226+
startTime: startDate.valueOf(),
227+
endTime: endDate.valueOf(),
228+
});
229+
const searchParameters = {
230+
logset_id: data.logsetId,
231+
topic_ids: data.topicId,
232+
start_time: startDate.format(TimeFormat),
233+
end_time: endDate.format(TimeFormat),
234+
// query_string 必须用 cam 特有的 url 编码方式
235+
query_string: sql,
236+
limit: data.limit || 10,
237+
sort: 'desc',
238+
};
239+
const { results = [] } = await clsClient.searchLog(searchParameters);
240+
const logs = [];
241+
for (let i = 0, len = results.length; i < len; i++) {
242+
const curReq = results[i];
243+
const detailLog = await this.getLogDetail({
244+
logsetId: data.logsetId,
245+
topicId: data.topicId,
246+
reqId: curReq.requestId,
247+
startTime: startDate.format(TimeFormat),
248+
endTime: endDate.format(TimeFormat),
249+
});
250+
curReq.message = (detailLog || [])
251+
.map(({ content }: { content: string }) => {
252+
try {
253+
const info = JSON.parse(content) as LogContent;
254+
if (info.SCF_Type === 'Custom') {
255+
curReq.memoryUsage = info.SCF_MemUsage;
256+
curReq.duration = info.SCF_Duration;
257+
}
258+
return info.SCF_Message;
259+
} catch (e) {
260+
return '';
261+
}
262+
})
263+
.join('');
264+
logs.push(curReq);
265+
}
266+
return logs;
267+
}
268+
async getLogDetail(data: GetLogDetailOptions) {
269+
const clsClient = new ClsClient({
270+
region: this.region,
271+
secretId: this.credentials.SecretId!,
272+
secretKey: this.credentials.SecretKey!,
273+
token: this.credentials.Token,
274+
debug: false,
275+
});
276+
277+
data.startTime = data.startTime || dayjs(data.endTime).add(-1, 'hour').format(TimeFormat);
278+
279+
const sql = `SCF_RequestId:${data.reqId} AND SCF_RetryNum:0`;
280+
const searchParameters = {
281+
logset_id: data.logsetId,
282+
topic_ids: data.topicId,
283+
start_time: data.startTime as string,
284+
end_time: data.endTime,
285+
// query_string 必须用 cam 特有的 url 编码方式
286+
query_string: sql,
287+
limit: 100,
288+
sort: 'asc',
289+
};
290+
const { results = [] } = await clsClient.searchLog(searchParameters);
291+
return results;
292+
}
195293
}

src/modules/cls/interface.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,81 @@ export interface ClsDeployInputs
3232
export interface ClsDeployOutputs extends Partial<ClsDeployInputs> {
3333
region: RegionType;
3434
}
35+
36+
export interface StatusSqlMapEnum {
37+
success: string;
38+
fail: string;
39+
retry: string;
40+
interrupt: string;
41+
timeout: string;
42+
exceed: string;
43+
codeError: string;
44+
}
45+
46+
export interface GetSearchSqlOptions {
47+
// 函数名称
48+
functionName: string;
49+
// 命名空间
50+
namespace?: string;
51+
// 函数版本
52+
qualifier?: string;
53+
// 开始时间
54+
startTime?: number | string;
55+
// 结束时间
56+
endTime?: number | string;
57+
// 请求 ID
58+
reqId?: string;
59+
// 日志状态
60+
status?: keyof StatusSqlMapEnum | '';
61+
62+
// 查询条数
63+
limit?: number;
64+
}
65+
66+
export type GetLogOptions = Omit<GetSearchSqlOptions, 'startTime'> & {
67+
logsetId: string;
68+
topicId: string;
69+
// 时间间隔,单位秒,默认为 3600s
70+
interval?: string | number;
71+
};
72+
73+
export type GetLogDetailOptions = {
74+
logsetId: string;
75+
topicId: string;
76+
reqId: string;
77+
// 开始时间
78+
startTime?: string;
79+
// 结束时间
80+
endTime: string;
81+
};
82+
83+
export interface LogContent {
84+
// 函数名称
85+
SCF_FunctionName: string;
86+
// 命名空间
87+
SCF_Namespace: string;
88+
// 开始时间
89+
SCF_StartTime: string;
90+
// 请求 ID
91+
SCF_RequestId: string;
92+
// 运行时间
93+
SCF_Duration: string;
94+
// 别名
95+
SCF_Alias: string;
96+
// 版本
97+
SCF_Qualifier: string;
98+
// 日志时间
99+
SCF_LogTime: string;
100+
// 重试次数
101+
SCF_RetryNum: string;
102+
// 使用内存
103+
SCF_MemUsage: string;
104+
// 日志等级
105+
SCF_Level: string;
106+
// 日志信息
107+
SCF_Message: string;
108+
// 日志类型
109+
SCF_Type: string;
110+
// 状态吗
111+
SCF_StatusCode: string;
112+
}

src/modules/cls/utils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Cls } from '@tencent-sdk/cls';
22
import { IndexRule } from '@tencent-sdk/cls/dist/typings';
33
import { ApiError } from '../../utils/error';
4+
import { StatusSqlMapEnum, GetSearchSqlOptions } from './interface';
45

56
export async function getLogsetByName(cls: Cls, data: { name: string }) {
67
const { logsets = [] } = await cls.getLogsetList();
@@ -240,3 +241,42 @@ export async function deleteClsTrigger(cls: Cls, data: { topic_id: string }) {
240241
}
241242
return res;
242243
}
244+
245+
const StatusSqlMap: StatusSqlMapEnum = {
246+
success: 'SCF_StatusCode=200',
247+
fail: 'SCF_StatusCode != 200 AND SCF_StatusCode != 202 AND SCF_StatusCode != 499',
248+
retry: 'SCF_RetryNum > 0',
249+
interrupt: 'SCF_StatusCode = 499',
250+
timeout: 'SCF_StatusCode = 433',
251+
exceed: 'SCF_StatusCode = 434',
252+
codeError: 'SCF_StatusCode = 500',
253+
};
254+
255+
export function formatWhere({
256+
functionName,
257+
namespace = 'default',
258+
qualifier = '$LATEST',
259+
status,
260+
startTime,
261+
endTime,
262+
}: Partial<GetSearchSqlOptions>) {
263+
let where = `SCF_Namespace='${namespace}' AND SCF_Qualifier='${qualifier}'`;
264+
if (startTime && endTime) {
265+
where += ` AND (SCF_StartTime between ${startTime} AND ${endTime})`;
266+
}
267+
if (functionName) {
268+
where += ` AND SCF_FunctionName='${functionName}'`;
269+
}
270+
if (status) {
271+
where += ` AND ${StatusSqlMap[status]}'`;
272+
}
273+
274+
return where;
275+
}
276+
277+
export function getSearchSql(options: GetSearchSqlOptions) {
278+
const where = formatWhere(options);
279+
const sql = `* | SELECT SCF_RequestId as requestId, SCF_RetryNum as retryNum, MAX(SCF_StartTime) as startTime WHERE ${where} GROUP BY SCF_RequestId, SCF_RetryNum ORDER BY startTime desc`;
280+
281+
return sql;
282+
}

0 commit comments

Comments
 (0)