diff --git a/.github/workflows/ci_node16.yaml b/.github/workflows/ci_node16.yaml index f2b16b2c..cb6b879f 100644 --- a/.github/workflows/ci_node16.yaml +++ b/.github/workflows/ci_node16.yaml @@ -19,8 +19,8 @@ jobs: macos-ci: runs-on: macos-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 16 registry-url: https://registry.npmjs.org/ @@ -29,9 +29,10 @@ jobs: with: go-version: 1.18 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 8 + distribution: zulu - name: install s run: | npm i @serverless-devs/s -g @@ -58,8 +59,8 @@ jobs: windows-ci: runs-on: windows-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 16 registry-url: https://registry.npmjs.org/ @@ -71,9 +72,10 @@ jobs: with: go-version: 1.18 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 8 + distribution: zulu - name: install s run: | npm i @serverless-devs/s -g @@ -102,8 +104,8 @@ jobs: linux-ci: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 16 registry-url: https://registry.npmjs.org/ @@ -112,9 +114,10 @@ jobs: with: go-version: 1.18 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 8 + distribution: zulu - name: install s run: | npm i @serverless-devs/s -g diff --git a/.github/workflows/ci_with_docker_linux.yaml b/.github/workflows/ci_with_docker_linux.yaml index e8337d92..65e94411 100644 --- a/.github/workflows/ci_with_docker_linux.yaml +++ b/.github/workflows/ci_with_docker_linux.yaml @@ -12,9 +12,9 @@ jobs: docker-ci-standard: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v2 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: node-version: 16 registry-url: https://registry.npmjs.org/ @@ -23,9 +23,10 @@ jobs: with: go-version: 1.18 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 8 + distribution: zulu - name: Install dependencies run: | sudo apt-get update @@ -68,9 +69,9 @@ jobs: docker-ci-custom: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v2 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: node-version: 16 registry-url: https://registry.npmjs.org/ @@ -79,9 +80,10 @@ jobs: with: go-version: 1.18 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: java-version: 8 + distribution: zulu - name: Install dependencies run: | sudo apt-get update diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5fdf869a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# Claude Code Configuration + +This file contains configuration information for Claude Code, an AI programming assistant. + +## Project Overview + +FC3 is the Serverless Devs component for Alibaba Cloud Function Compute 3.0, providing full lifecycle management for serverless functions. Written in TypeScript with modular architecture supporting create, develop, debug, deploy, and operate workflows. + +**Tech Stack**: TypeScript 4.4, Jest, Vercel ncc, f2elint, Prettier + +## Available Scripts + +| Script | Description | +| ------------------------- | -------------------------- | +| `npm run build` | Production bundle with ncc | +| `npm run watch` | TypeScript watch mode | +| `npm test` | Jest tests with coverage | +| `npm run format` | Prettier formatting | +| `npm run lint` | f2elint scanning | +| `npm run fix` | Auto-fix lint issues | +| `npm run publish` | Build and registry publish | +| `npm run generate-schema` | Generate JSON schema | + +## Key Directories + +| Directory | Purpose | +| ------------------ | -------------------------------------------- | +| `src/` | Source code | +| `src/subCommands/` | CLI subcommands (deploy, build, local, etc.) | +| `src/resources/` | Cloud resources (FC, RAM, SLS, OSS, ACR) | +| `src/interface/` | TypeScript interfaces | +| `src/utils/` | Utility functions | +| `__tests__/ut/` | Unit tests | +| `__tests__/it/` | Integration tests | +| `docs/` | Documentation | + +## Testing + +**Current Status**: 986 tests total, 986 passing, 2 skipped (integration tests require cloud credentials) + +**Run tests**: `npm test` +**Coverage**: Run with `--coverage` flag + +## Architecture + +Modular architecture with: + +1. Main entry (`index.ts`) routing to subcommands +2. Base class (`base.ts`) with common preprocessing +3. Subcommand modules for operations +4. Resource modules for cloud service integration + +See `docs/architecture.md` for detailed diagrams. + +## Recent Features + +- HTTP URL support for code/layer sources (v0.1.17) +- Layer publish HTTP bug fix (PR #147) +- FileManager remove operation upgrade support +- ProvisionConfig/ScalingConfig array handling +- LLM metrics in logConfig +- Logs command: multi-topic search (FCLogs + FCInstanceEvents) for --instance-id, SLS field-specific query syntax + +## Development Workflow + +1. Create branch from `master` +2. Run `npm run lint` and `npm run format` +3. Write/update tests +4. Build: `npm run build` +5. Test: `npm test` +6. Submit PR + +## Documentation Index + +| File | Purpose | +| ---------------------- | -------------------- | +| `docs/CONTRIB.md` | Development guide | +| `docs/RUNBOOK.md` | Operations runbook | +| `docs/architecture.md` | Architecture details | diff --git a/__tests__/ut/commands/logs_test.ts b/__tests__/ut/commands/logs_test.ts index 43918424..2ac59cb6 100644 --- a/__tests__/ut/commands/logs_test.ts +++ b/__tests__/ut/commands/logs_test.ts @@ -177,10 +177,13 @@ describe('Logs', () => { projectName: 'test-project', logStoreName: 'test-logstore', topic: 'FCLogs:test-function', + topicFilter: '__topic__:"FCLogs:test-function"', query: '', search: '', qualifier: '', match: '', + requestId: '', + instanceId: '', }; // Call the single iteration method await this._realtimeOnce(params); @@ -212,6 +215,8 @@ describe('Logs', () => { projectName: 'test-project', logStoreName: 'test-logstore', topic: 'FCLogs:test-function', + topicFilter: '__topic__:"FCLogs:test-function"', + functionName: 'test-function', }), ); }); @@ -244,8 +249,29 @@ describe('Logs', () => { const props = await (logs as any).getInputs(); expect(props.topic).toBe('test-function'); + expect(props.topicFilter).toBe('__topic__:"test-function"'); expect(props.query).toBe('LATEST'); }); + + it('should include FCInstanceEvents topic when instance-id is specified', async () => { + mockInputs.args = ['--instance-id', 'c-69f8a959-15f8e4fe-b867da209124']; + logs = new Logs(mockInputs); + + const props = await (logs as any).getInputs(); + + expect(props.topicFilter).toBe( + '(__topic__:"FCLogs:test-function" or __topic__:"FCInstanceEvents:/test-function")', + ); + }); + + it('should not unset LATEST qualifier', async () => { + mockInputs.args = ['--qualifier', 'LATEST']; + logs = new Logs(mockInputs); + + const props = await (logs as any).getInputs(); + + expect(props.qualifier).toBe('LATEST'); + }); }); describe('getFunction', () => { @@ -291,6 +317,7 @@ describe('Logs', () => { projectName: 'test-project', logStoreName: 'test-logstore', topic: 'FCLogs:test-function', + topicFilter: '__topic__:"FCLogs:test-function"', query: '', search: '', type: '', @@ -299,6 +326,7 @@ describe('Logs', () => { qualifier: '', startTime: '', endTime: '', + functionName: 'test-function', }; const result = await (logs as any).history(params); @@ -318,6 +346,7 @@ describe('Logs', () => { projectName: 'test-project', logStoreName: 'test-logstore', topic: 'FCLogs:test-function', + topicFilter: '__topic__:"FCLogs:test-function"', query: '', search: '', type: '', @@ -326,6 +355,7 @@ describe('Logs', () => { qualifier: '', startTime: '2023-01-01T00:00:00Z', endTime: '2023-01-01T01:00:00Z', + functionName: 'test-function', }; const result = await (logs as any).history(params); @@ -338,6 +368,7 @@ describe('Logs', () => { projectName: 'test-project', logStoreName: 'test-logstore', topic: 'FCLogs:test-function', + topicFilter: '__topic__:"FCLogs:test-function"', query: '', search: '', type: '', @@ -346,6 +377,7 @@ describe('Logs', () => { qualifier: '', startTime: 'invalid-date', endTime: 'also-invalid', + functionName: 'test-function', }; await expect((logs as any).history(params)).rejects.toThrow( @@ -367,10 +399,13 @@ describe('Logs', () => { projectName: 'test-project', logStoreName: 'test-logstore', topic: 'FCLogs:test-function', + topicFilter: '__topic__:"FCLogs:test-function"', query: '', search: '', qualifier: '', match: '', + requestId: '', + instanceId: '', }; // We'll only run one iteration in the test @@ -526,9 +561,12 @@ describe('Logs', () => { 'LATEST', 'req-123', 'inst-456', + '__topic__:"FCLogs:test-function"', ); - expect(result).toBe('baseQuery and searchTerm and LATEST and inst-456 and req-123'); + expect(result).toBe( + '__topic__:"FCLogs:test-function" and baseQuery and searchTerm and qualifier: "LATEST" and instanceID: "inst-456" and requestId: "req-123"', + ); }); it('should generate SLS query with some parameters', () => { @@ -542,6 +580,47 @@ describe('Logs', () => { expect(result).toBe(''); }); + + it('should generate topicFilter with FCInstanceEvents when instanceId is specified', () => { + const result = (logs as any).getSlsQuery( + null, + null, + null, + null, + 'inst-456', + '(__topic__:"FCLogs:test-function" or __topic__:"FCInstanceEvents:/test-function")', + ); + + expect(result).toBe( + '(__topic__:"FCLogs:test-function" or __topic__:"FCInstanceEvents:/test-function") and instanceID: "inst-456"', + ); + }); + + it('should generate query with topicFilter only', () => { + const result = (logs as any).getSlsQuery( + null, + null, + null, + null, + null, + '__topic__:"FCLogs:test-function"', + ); + + expect(result).toBe('__topic__:"FCLogs:test-function"'); + }); + + it('should use field-specific syntax for qualifier', () => { + const result = (logs as any).getSlsQuery( + null, + null, + 'LATEST', + null, + null, + '__topic__:"FCLogs:test-function"', + ); + + expect(result).toBe('__topic__:"FCLogs:test-function" and qualifier: "LATEST"'); + }); }); describe('compareLogConfig', () => { diff --git a/docs/architecture.md b/docs/architecture.md index db72ab12..580f9cce 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -243,6 +243,8 @@ src/ - **核心功能**: - 项目名称生成 - 日志存储名称生成 + - SLS 查询语句生成(支持字段查询语法) + - 多 topic 搜索(FCLogs、FCInstanceEvents) #### 4.4 VPC-NAS 网络存储 (vpc-nas/) diff --git a/docs/technical-documentation.md b/docs/technical-documentation.md index 48b3a8d4..894082b4 100644 --- a/docs/technical-documentation.md +++ b/docs/technical-documentation.md @@ -446,8 +446,30 @@ s logs --tail # 查询特定时间段的日志 s logs --start-time 2023-01-01T00:00:00Z --end-time 2023-01-01T23:59:59Z + +# 查询指定实例的日志(同时搜索 FCLogs 和 FCInstanceEvents 两个 topic) +s logs --instance-id c-69f8a959-15f8e4fe-b867da209124 + +# 查询指定请求的日志 +s logs --request-id 0f7032f1-ffde-474e-92ce-188210368b53 + +# 查询指定版本的日志 +s logs --qualifier LATEST ``` +#### SLS Topic 类型 + +FC3 的 SLS logstore 包含以下 topic 类型: + +| Topic 格式 | 说明 | +| --------------------------------- | ------------------------------- | +| `FCLogs:/functionName` | 函数调用日志 | +| `FCInstanceEvents:/functionName` | 实例生命周期事件(创建/销毁等) | +| `FCRequestMetrics:/functionName` | 请求指标 | +| `FCInstanceMetrics:/functionName` | 实例指标 | + +默认 `s logs` 只查询 `FCLogs` topic。使用 `--instance-id` 参数时会同时搜索 `FCLogs` 和 `FCInstanceEvents` 两个 topic。 + ## 错误处理 ### 常见错误类型 diff --git a/src/commands-help/logs.ts b/src/commands-help/logs.ts index d80b4275..d2bec7d1 100644 --- a/src/commands-help/logs.ts +++ b/src/commands-help/logs.ts @@ -17,7 +17,10 @@ Examples with CLI: ], ['--function-name ', '[C-Required] Specify function name'], ['--request-id ', '[Optional] Query according to requestId'], - ['--instance-id ', '[Optional] Query according to instanceId'], + [ + '--instance-id ', + '[Optional] Query according to instanceId, also searches FCInstanceEvents topic', + ], [ '-s, --start-time', '[Optional] Query log start time (timestamp or time format,like 1611827290000 or 2021-11-11T11:11:12+00:00)', diff --git a/src/index.ts b/src/index.ts index 0531e97a..bc96c3d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,4 @@ // Suppress Node.js deprecation warnings from third-party dependencies (DEP0005: Buffer constructor) -(process as any).noDeprecation = true; - import * as fs from 'fs'; import { parseArgv } from '@serverless-devs/utils'; import { IInputs } from './interface'; @@ -33,6 +31,8 @@ import { SCHEMA_FILE_PATH } from './constant'; import { checkDockerIsOK, isAppCenter, isYunXiao } from './utils'; import { Model } from './subCommands/model'; +(process as any).noDeprecation = true; + export default class Fc extends Base { // 部署函数 public async deploy(inputs: IInputs) { diff --git a/src/subCommands/logs/index.ts b/src/subCommands/logs/index.ts index e9c9cc1c..ecd1944f 100644 --- a/src/subCommands/logs/index.ts +++ b/src/subCommands/logs/index.ts @@ -16,7 +16,7 @@ interface IGetLogs { logStoreName: string; from: string | number; to: string | number; - topic: string; + topic?: string; query: string; } @@ -24,23 +24,25 @@ interface IRealtime { projectName: string; logStoreName: string; topic: string; + topicFilter: string; query: string; search: string; qualifier: string; match: string; + requestId?: string; + instanceId?: string; } interface IHistory extends IRealtime { startTime: string; endTime: string; type: 'success' | 'fail' | 'failed'; - requestId: string; - instanceId: string; } interface IProps extends IHistory { region: string; tail: boolean; + functionName?: string; } const replaceAll = (string, search, replace) => string.split(search).join(replace); @@ -160,18 +162,22 @@ export default class Logs { } } - if (this.opts?.qualifier && this.opts.qualifier === 'LATEST') { - _.unset(this.opts, 'qualifier'); - } - let topic: string; + let topicFilter: string; let query: string; if (functionName.indexOf('$') >= 0) { topic = functionName.split('$')[0]; + topicFilter = `__topic__:"${topic}"`; query = functionName.split('$')[1]; } else { topic = `FCLogs:${functionName}`; + const instanceIdValue = this.opts?.['instance-id']; + if (!_.isNil(instanceIdValue)) { + topicFilter = `(__topic__:"FCLogs:${functionName}" or __topic__:"FCInstanceEvents:/${functionName}")`; + } else { + topicFilter = `__topic__:"FCLogs:${functionName}"`; + } query = this.opts?.query || props?.query; } logger.debug(topic, query); @@ -181,6 +187,7 @@ export default class Logs { projectName: logConfig.project, logStoreName: logstore, topic, + topicFilter, query, tail: this.opts?.tail, startTime: this.opts?.['start-time'] || new Date().getTime() - 60 * 60 * 1000, @@ -191,6 +198,7 @@ export default class Logs { match: this.opts?.match, requestId: this.opts?.['request-id'], instanceId: this.opts?.['instance-id'], + functionName, }; } @@ -299,7 +307,17 @@ export default class Logs { /** * 获取实时日志 */ - async realtime({ projectName, logStoreName, topic, query, search, qualifier, match }: IRealtime) { + async realtime({ + projectName, + logStoreName, + topicFilter, + query, + search, + qualifier, + match, + requestId, + instanceId, + }: IRealtime) { let timeStart; let timeEnd; let times = 1800; @@ -320,8 +338,7 @@ export default class Logs { const pulledlogs = await this.getLogs({ projectName, logStoreName, - topic, - query: this.getSlsQuery(query, search, qualifier), + query: this.getSlsQuery(query, search, qualifier, requestId, instanceId, topicFilter), from: timeStart, to: timeEnd, }); @@ -353,11 +370,13 @@ export default class Logs { async _realtimeOnce({ projectName, logStoreName, - topic, + topicFilter, query, search, qualifier, match, + requestId, + instanceId, }: IRealtime) { const timeStart = moment().subtract(10, 'seconds').unix(); const timeEnd = moment().unix(); @@ -366,8 +385,7 @@ export default class Logs { const pulledlogs = await this.getLogs({ projectName, logStoreName, - topic, - query: this.getSlsQuery(query, search, qualifier), + query: this.getSlsQuery(query, search, qualifier, requestId, instanceId, topicFilter), from: timeStart, to: timeEnd, }); @@ -397,7 +415,7 @@ export default class Logs { async history({ projectName, logStoreName, - topic, + topicFilter, query, search, type, @@ -427,8 +445,7 @@ export default class Logs { to, projectName, logStoreName, - topic, - query: this.getSlsQuery(query, search, qualifier, requestId, instanceId), + query: this.getSlsQuery(query, search, qualifier, requestId, instanceId, topicFilter), }; const logsList = await this.getLogs(params); @@ -444,12 +461,13 @@ export default class Logs { qualifier: string, requestId?: string, instanceId?: string, + topicFilter?: string, ): string { - let q = ''; - let hasValue = false; + let q = topicFilter || ''; + let hasValue = !!topicFilter; if (!_.isNil(query)) { - q += query; + q = hasValue ? `${q} and ${query}` : query; hasValue = true; } @@ -459,17 +477,17 @@ export default class Logs { } if (!_.isNil(qualifier)) { - q = hasValue ? `${q} and ${qualifier}` : qualifier; + q = hasValue ? `${q} and qualifier: "${qualifier}"` : `qualifier: "${qualifier}"`; hasValue = true; } if (!_.isNil(instanceId)) { - q = hasValue ? `${q} and ${instanceId}` : instanceId; + q = hasValue ? `${q} and instanceID: "${instanceId}"` : `instanceID: "${instanceId}"`; hasValue = true; } if (!_.isNil(requestId)) { - q = hasValue ? `${q} and ${requestId}` : requestId; + q = hasValue ? `${q} and requestId: "${requestId}"` : `requestId: "${requestId}"`; } return q; @@ -480,6 +498,10 @@ export default class Logs { */ async getLogs(requestParams: IGetLogs, tabReplaceStr = '\n') { this.logger.debug(`get logs params: ${JSON.stringify(requestParams)}`); + // Topic filtering is handled by __topic__ in query, remove topic from SLS request params + const slsParams: any = { ...requestParams }; + delete slsParams.topic; + let count; let xLogCount; let xLogProgress = 'Complete'; @@ -488,7 +510,7 @@ export default class Logs { do { const response: any = await new Promise((resolve, reject) => { - this.slsClient.getLogs(requestParams, (error, data) => { + this.slsClient.getLogs(slsParams, (error, data) => { if (error) { reject(error); }