From 86d1ec7044b5076e01252e22c7f0f33ee3ff201e Mon Sep 17 00:00:00 2001 From: ls147258 Date: Mon, 4 May 2026 23:55:33 +0800 Subject: [PATCH 1/3] fix: logs command multi-topic search and SLS field query syntax - Search FCInstanceEvents topic alongside FCLogs when --instance-id is specified - Use SLS field-specific query syntax: qualifier: "value", instanceID: "value", requestId: "value" - Stop unsetting LATEST qualifier (was preventing LATEST version log searches) - Support --request-id and --instance-id filters in realtime (--tail) mode - Update docs and help text for the new behavior Change-Id: Id05f45be1e1597dd89682a2ebab336b68735ca89 Co-developed-by: Claude --- CLAUDE.md | 79 +++++++++++++++++++++++++++++ __tests__/ut/commands/logs_test.ts | 81 +++++++++++++++++++++++++++++- docs/architecture.md | 2 + docs/technical-documentation.md | 22 ++++++++ src/commands-help/logs.ts | 5 +- src/index.ts | 4 +- src/subCommands/logs/index.ts | 67 ++++++++++++++++-------- 7 files changed, 235 insertions(+), 25 deletions(-) create mode 100644 CLAUDE.md 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..e6e02eae 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,18 @@ export default class Logs { /** * 获取实时日志 */ - async realtime({ projectName, logStoreName, topic, query, search, qualifier, match }: IRealtime) { + async realtime({ + projectName, + logStoreName, + topic, + topicFilter, + query, + search, + qualifier, + match, + requestId, + instanceId, + }: IRealtime) { let timeStart; let timeEnd; let times = 1800; @@ -320,8 +339,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, }); @@ -354,10 +372,13 @@ export default class Logs { projectName, logStoreName, topic, + topicFilter, query, search, qualifier, match, + requestId, + instanceId, }: IRealtime) { const timeStart = moment().subtract(10, 'seconds').unix(); const timeEnd = moment().unix(); @@ -366,8 +387,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, }); @@ -398,6 +418,7 @@ export default class Logs { projectName, logStoreName, topic, + topicFilter, query, search, type, @@ -427,8 +448,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 +464,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 +480,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 +501,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 +513,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); } From a698c00993483713e1df71d757fabc53efc8fec1 Mon Sep 17 00:00:00 2001 From: ls147258 Date: Tue, 5 May 2026 09:07:15 +0800 Subject: [PATCH 2/3] fix: remove unused topic destructuring variable in logs command methods Change-Id: I2e55f6cfb2ac1a741f26bf8db8f959eb5570c637 Co-developed-by: Claude --- src/subCommands/logs/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/subCommands/logs/index.ts b/src/subCommands/logs/index.ts index e6e02eae..ecd1944f 100644 --- a/src/subCommands/logs/index.ts +++ b/src/subCommands/logs/index.ts @@ -310,7 +310,6 @@ export default class Logs { async realtime({ projectName, logStoreName, - topic, topicFilter, query, search, @@ -371,7 +370,6 @@ export default class Logs { async _realtimeOnce({ projectName, logStoreName, - topic, topicFilter, query, search, @@ -417,7 +415,6 @@ export default class Logs { async history({ projectName, logStoreName, - topic, topicFilter, query, search, From f634129a0016369bb8d53f8c2cb6c72edbd6b405 Mon Sep 17 00:00:00 2001 From: ls147258 Date: Tue, 5 May 2026 09:54:24 +0800 Subject: [PATCH 3/3] fix: upgrade setup-java to v4 with zulu distribution to fix JAVA_HOME on macOS runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup-java@v1 fails to correctly set JAVA_HOME on newer macOS runners. Temurin doesn't support Java 8 on macOS, so use zulu distribution instead. Also upgrade checkout@v2→v4 and setup-node@v2→v4. Change-Id: Ibb3997e6df9cb3c744a1a35fbd90160b276f80b1 Co-developed-by: Claude --- .github/workflows/ci_node16.yaml | 21 ++++++++++++--------- .github/workflows/ci_with_docker_linux.yaml | 14 ++++++++------ 2 files changed, 20 insertions(+), 15 deletions(-) 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