Compare commits

...

259 Commits

Author SHA1 Message Date
Kevin Wan
0f2b589d4d Revert "fix: api group set timeout: 0s not working." (#4931) 2025-06-08 23:14:38 +08:00
spectatorMrZ
19fec36d24 fix: api group set timeout: 0s not working. (#4785) 2025-06-08 14:50:21 +00:00
Kevin Wan
f037bf344d chore: add more tests (#4930) 2025-06-08 22:08:04 +08:00
MarkJoyMa
d99cf35b07 Feat/continue profiling (#4867)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
Co-authored-by: aiden.ma <aiden.ma@bkyo.io>
2025-06-07 21:12:31 +08:00
Kevin Wan
f459f1b5ff chore: update goctl version (#4929) 2025-06-07 21:01:35 +08:00
Haiwei Zhang
0140fd417b feat(goctl): generate mongo model with cache prefix (#4907) 2025-06-07 12:54:33 +00:00
jaron
7969e0ca38 fix(goctl): Fix getting swagger consume types (#4903) 2025-06-07 12:46:34 +00:00
Kevin Wan
91c885b5b0 chore: add more unit tests for mcp (#4928) 2025-06-07 20:41:57 +08:00
MarkJoyMa
d4cccca387 Fix the problem that mcp request id is not of int type (#4914) 2025-06-07 10:37:18 +08:00
dependabot[bot]
4b2095ed03 chore(deps): bump github.com/redis/go-redis/v9 from 9.9.0 to 9.10.0 (#4926) 2025-06-07 10:07:26 +08:00
dependabot[bot]
1229eeb2d2 chore(deps): bump go.mongodb.org/mongo-driver from 1.17.3 to 1.17.4 (#4924) 2025-06-06 19:45:26 +08:00
dependabot[bot]
9142b146c5 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.34.0 to 2.35.0 (#4919)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 10:09:15 +08:00
Kevin Wan
8a1b2d5aed chore: fix typo (#4920) 2025-06-05 22:51:22 +08:00
Leon cap
da5d39e6ca fix: correct spelling of 'cancellation' in timeout handler comment (#4916)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2025-06-05 22:42:53 +08:00
Leon cap
68c5a17c67 fix: correct spelling of 'underlying' in Header method comment (#4918) 2025-06-05 10:36:21 +00:00
Leon cap
b53f9f5f2d fix: correct spelling of 'TimeoutHandler' in timeout handler comment (#4917) 2025-06-04 15:48:37 +00:00
dependabot[bot]
36d57626b6 chore(deps): bump github.com/redis/go-redis/v9 from 9.8.0 to 9.9.0 (#4905)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-28 11:32:57 +08:00
Kevin Wan
4e36ba832f Update readme.md (#4897) 2025-05-25 22:25:56 +08:00
Kevin Wan
a44954a771 fix: don't set read/write timeout if timeout middleware disabled (#4895)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-05-25 15:07:58 +08:00
kesonan
f3edd4b880 goctl: v1.8.4-beta (#4890) 2025-05-25 05:36:56 +00:00
Kevin Wan
2de3e397ff chore: revert go version to 1.21 (#4893) 2025-05-24 18:17:49 +08:00
Qiu shao
a435eb56f2 perf(hash): optimize Md5Hex encoding performance (#4891) 2025-05-24 08:41:21 +00:00
Kevin Wan
d80761c147 chore: refactor coding style (#4887) 2025-05-22 23:29:40 +08:00
me-cs
e7bd0d8b60 update:To standardize the time format, use the go standard library's own (#4875) 2025-05-22 15:26:53 +00:00
me-cs
b109b3ef4c update:use builtin cmp func (#4879) 2025-05-22 15:19:13 +00:00
Kevin Wan
e3c371ac89 chore: refactor 2025-05-20 12:59:35 +00:00
燕归来
15eb6f4f6d test(hash): modify TestConsistentHashTransferOnFailure to more reasonable test transfer ratio (#4874) 2025-05-20 12:51:50 +00:00
me-cs
4d3681b71c Optimize slicing operations (#4877) 2025-05-20 11:36:02 +00:00
dependabot[bot]
a682bda0bb chore(deps): bump github.com/jackc/pgx/v5 from 5.7.4 to 5.7.5 (#4871)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 10:19:06 +08:00
kesonan
45b27ad93a goctl: 1.8.4-beta (#4869) 2025-05-19 15:30:50 +00:00
Kevin Wan
292a8302a1 chore: optimize mcp (#4866) 2025-05-17 12:28:06 +08:00
kesonan
91ab1f6d2b goctl features of 1.8.4-alpha (#4849) 2025-05-15 13:59:48 +00:00
Kevin Wan
5048c350ae chore: fix test failure in profilecenter_test.go 2025-05-15 13:31:53 +00:00
Kevin Wan
94edc32f3e chore: optimize profile center and remove tablewriter dependency 2025-05-15 13:22:27 +00:00
Kevin Wan
ec989b2e2a chore: for backward compatibility (#4852)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-05-11 20:19:00 +08:00
me-cs
82fe802e81 update:Use the official sync.OnceFunc (#4840) 2025-05-11 12:08:43 +00:00
me-cs
072d68f897 update:Use the official slice operate func (#4841) 2025-05-11 11:48:54 +00:00
Kevin Wan
2e91ba5811 chore: refactor rest file server (#4851)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-05-11 12:44:43 +08:00
shaouai
5564c43197 feat: serve files using embed.FS (#4847) 2025-05-10 15:43:13 +00:00
Kevin Wan
e55158b0f7 chore: update deps in goctl (#4830) 2025-05-04 16:18:02 +08:00
Kevin Wan
69aa7fe346 feat: improve mcp (#4828)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-05-04 15:29:14 +08:00
kesonan
c3820a95c1 release goctl swagger (#4829) 2025-05-04 06:05:41 +00:00
Kevin Wan
493f3bad0f fix: allow special characters like periods in API route paths (#4827) 2025-05-04 11:07:43 +08:00
Kevin Wan
eb0d5ad3a4 Update readme-cn.md (#4826) 2025-05-02 17:31:49 +08:00
wwwfeng
14192050ae fix: goctl api tsgen (#4726) 2025-05-02 09:15:32 +00:00
spectatorMrZ
9193e771e3 fix: pg gen model missing cache prefix (#4788)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2025-05-02 08:52:55 +00:00
hoshi
808b4e496a feat:add cache prefix support to pg (#4741)
Co-authored-by: 郑好 <zheng.hao1@outlook.com>
2025-05-02 08:10:47 +00:00
kesonan
e416d01f8d api format from stdin (#4772) 2025-05-02 07:59:47 +00:00
Joe Bird
789c5de873 fix(marshaler): fix bug when marshal array (#4790) 2025-05-02 07:54:00 +00:00
Rankgice
52078a0c14 Fix the issue of generating swagger @doc "xxx" that fails, and use th… (#4816)
Co-authored-by: lxr <qiyuechuqi@an-idear.com>
2025-05-02 07:06:03 +00:00
Qiu shao
7ef13116a0 feat: add http to http method test case (#4820) 2025-05-02 06:59:36 +00:00
dependabot[bot]
6b8053410a chore(deps): bump github.com/redis/go-redis/v9 from 9.7.3 to 9.8.0 (#4821)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-02 14:48:30 +08:00
dependabot[bot]
81c6928445 chore(deps): bump github.com/emicklei/proto from 1.14.0 to 1.14.1 in /tools/goctl (#4817)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-02 14:29:42 +08:00
Kevin Wan
761c2dd716 chore: update goctl version (#4822) 2025-05-02 14:18:54 +08:00
kesonan
aeceb3cfbe Update goctl version to 1.8.3-beta (#4812) 2025-04-27 15:53:49 +00:00
kesonan
15ea07aad1 goctl: support custom swagger authentication (#4811) 2025-04-27 15:43:37 +00:00
shaouai
98bebbc74f feat(swagger): allow users to specify the generated swagger file name (#4809) 2025-04-27 15:34:52 +00:00
kesonan
eafd11d949 goctl: supported api types group for EXPERIMENTAL(实验性功能:支持 api type 结构体按照分组名称拆分文件) (#4810) 2025-04-27 15:18:33 +00:00
Kevin Wan
b251ce346e feat: mcp server sdk (#4794)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-04-27 23:06:37 +08:00
kesonan
812140ba36 fix: goctl swagger missing security definition and submit json body data error (#4808) 2025-04-25 14:58:45 +00:00
kesonan
44735e949c fix array schmea generation incorrect (#4801) 2025-04-23 23:59:01 +00:00
kesonan
bf313c3c56 fix: swagger separator incorrect in Windows OS (#4799) 2025-04-23 13:51:02 +00:00
kesonan
94e7753262 fix: the parameter "required" in the Swagger document generated for repair is incorrect (#4791) 2025-04-21 04:18:02 +00:00
kesonan
9c478626d2 feature/goctl-api-swagger (#4780) 2025-04-17 14:38:55 +00:00
Kevin Wan
801c283478 Delete issue-translator.yml 2025-04-10 12:01:21 +08:00
Kevin Wan
2a54faf997 chore: coding style (#4771) 2025-04-10 09:28:42 +08:00
Hanggang Z
ecd98f3653 chore: add more orm_test (#4766) 2025-04-09 13:49:00 +00:00
soasurs
61641581eb fix: form fields of request optional (#4755)
Signed-off-by: soasurs <soasurs@gmail.com>
2025-04-08 13:05:21 +00:00
Kevin Wan
6f2730d5ae chore: update goctl version (#4754) 2025-04-06 19:09:02 +08:00
dependabot[bot]
0eff777b62 chore(deps): bump github.com/jackc/pgx/v5 from 5.7.2 to 5.7.4 (#4737)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 22:41:55 +08:00
dependabot[bot]
cafbf535f7 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#4734) 2025-03-26 16:28:36 +08:00
Kevin Wan
6edfce63e3 feat: add rest.WithSSE to build SSE route easier (#4729) 2025-03-22 13:38:13 +08:00
dependabot[bot]
cdb0098b18 chore(deps): bump github.com/redis/go-redis/v9 from 9.7.1 to 9.7.3 (#4722) 2025-03-21 09:51:31 +08:00
Kevin Wan
620c7f9693 chore: add more tests (#4718) 2025-03-19 23:54:04 +08:00
Meng Ye
dba444a382 feat: support redis getdel command (#4709) 2025-03-19 23:40:14 +08:00
dependabot[bot]
b24fb3ebf7 chore(deps): bump github.com/fullstorydev/grpcurl from 1.9.2 to 1.9.3 (#4701) 2025-03-12 12:05:28 +08:00
POABOB
967f0926eb fix: fix the bug of the numeric/decimal data type in pg (#4686) 2025-03-07 11:12:02 +00:00
dependabot[bot]
e68c683df9 chore(deps): bump github.com/prometheus/client_golang from 1.21.0 to 1.21.1 (#4683)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 11:14:23 +08:00
Kevin Wan
247985a065 chore: refactoring & add more tests (#4677) 2025-03-02 20:11:10 +08:00
anlynn
80573af0d8 feat: Add support for serialization of anonymous fields in HTTP client(httpc) (#4676)
Co-authored-by: 李安琳 <anlynn@gmail.com>
2025-03-02 19:10:50 +08:00
Kevin Wan
c0394b631a chore: fix display problems in version-check workflow (#4675) 2025-03-01 22:18:49 +08:00
Kevin Wan
68d1aba377 chore: upgrade go-zero version in goctl (#4674) 2025-03-01 21:23:23 +08:00
Kevin Wan
3315e60272 chore: performance tunning for stable runner (#4670) 2025-02-26 19:19:24 +08:00
dependabot[bot]
327ef73700 chore(deps): bump go.mongodb.org/mongo-driver from 1.17.2 to 1.17.3 (#4669) 2025-02-26 10:03:43 +08:00
dependabot[bot]
eb11521655 chore(deps): bump github.com/redis/go-redis/v9 from 9.7.0 to 9.7.1 (#4665) 2025-02-22 08:19:28 +08:00
Kevin Wan
4c37545e55 Update readme-cn.md (#4664) 2025-02-20 21:13:33 +08:00
dependabot[bot]
2f47c1fba4 chore(deps): bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0 (#4663) 2025-02-20 11:14:13 +08:00
dependabot[bot]
16d54d0ace chore(deps): bump github.com/go-sql-driver/mysql from 1.8.1 to 1.9.0 in /tools/goctl (#4662)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-19 11:38:41 +08:00
dependabot[bot]
9925bcbf99 chore(deps): bump github.com/go-sql-driver/mysql from 1.8.1 to 1.9.0 (#4661)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-19 11:13:41 +08:00
dependabot[bot]
38a5ecb796 chore(deps): bump github.com/spf13/cobra from 1.8.1 to 1.9.1 in /tools/goctl (#4660)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 10:47:42 +08:00
Kevin Wan
af78fc7c5f chore: add more tests (#4656) 2025-02-14 23:43:34 +08:00
Kevin Wan
790302b486 fix: should not ignore slowThreshold (#4655) 2025-02-14 23:14:57 +08:00
Nanosk07
6a0672b801 fix: SlowThreshold configuration not taking effect (#4654) 2025-02-14 14:56:25 +00:00
Kevin Wan
560c61612c chore: refactor (#4652) 2025-02-14 09:41:54 +08:00
Kevin Wan
6a988dc4a9 fix: make test purpose method private (#4649) 2025-02-14 00:31:53 +08:00
Kevin Wan
15842c3c7a Create version-check.yml (#4646) 2025-02-13 19:39:22 +08:00
Rui Chen
f2914a74df fix: update version to match with the release (#4645) 2025-02-13 01:11:09 +00:00
Kevin Wan
f113d512e8 chore: coding style (#4644) 2025-02-12 23:48:39 +08:00
kesonan
7a4818da59 Generate caches that support custom key prefix. (#4643) 2025-02-12 15:31:30 +00:00
dependabot[bot]
48d0709ca6 chore(deps): bump golang.org/x/net from 0.34.0 to 0.35.0 (#4640) 2025-02-11 09:25:16 +08:00
Kevin Wan
f747585518 chore: simplify http query array parsing (#4637) 2025-02-09 01:00:52 +08:00
xuerbujia
507ff96546 feat add tag switch to disable form array of split comma format (#4633)
Co-authored-by: wuhongyu <readboy@DESKTOP-T8INU17>
2025-02-09 00:34:41 +08:00
Kevin Wan
651eabb4c6 chore: refactor gateway http context (#4636) 2025-02-08 21:18:07 +08:00
#Suyghur
e6b4372056 fix(gateway): fixed http gateway context propagation error (#4634) 2025-02-08 09:50:26 +00:00
Kevin Wan
24073969a1 fix: redis username not working in redis v7 (#4632) 2025-02-08 12:21:35 +08:00
Kevin Wan
ca797ed22c chore: add trailing newlines (#4631) 2025-02-07 23:39:02 +08:00
youzipi
e347d3f8f8 fix(goctl): allow duplicate_path_expression under different prefix (#4626) 2025-02-07 08:37:11 +00:00
dependabot[bot]
396393b336 chore(deps): bump google.golang.org/protobuf from 1.36.4 to 1.36.5 (#4627) 2025-02-07 10:05:22 +08:00
dependabot[bot]
1f0531b254 chore(deps): bump google.golang.org/protobuf from 1.36.4 to 1.36.5 in /tools/goctl (#4628) 2025-02-07 08:25:54 +08:00
ningzi
77fb271a06 feat(goctl): support go work (#4332) (#4344) 2025-02-05 14:22:40 +00:00
dependabot[bot]
af7cf79963 chore(deps): bump golang.org/x/time from 0.9.0 to 0.10.0 (#4621)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 22:00:54 +08:00
dependabot[bot]
7926d396d7 chore(deps): bump golang.org/x/text from 0.21.0 to 0.22.0 in /tools/goctl (#4620)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-05 10:16:06 +08:00
dependabot[bot]
080cd3df84 chore(deps): bump golang.org/x/sys from 0.29.0 to 0.30.0 (#4622) 2025-02-05 09:27:03 +08:00
Kevin Wan
c4e1a6a2d8 chore: refactor mapreduce (#4619) 2025-02-01 00:12:37 +08:00
Kevin Wan
4e71e95e44 chore: add comments (#4618) 2025-01-31 22:42:46 +08:00
JiChen
84db9bcd15 fix: global fields apply to Third-party log module (#4400) 2025-01-31 13:51:20 +00:00
dependabot[bot]
b28f79ac11 chore(deps): bump github.com/spf13/pflag from 1.0.5 to 1.0.6 in /tools/goctl (#4615) 2025-01-30 16:49:47 +08:00
Kevin Wan
e134e77b2b chore: update go-zero to v1.7.6 for goctl (#4614) 2025-01-29 12:47:23 +08:00
Kevin Wan
f669d84ce8 chore: not using goproxy by default (#4613) 2025-01-29 12:28:47 +08:00
Kevin Wan
9213b8ac27 chore: update go-zero to v1.8.0 for goctl (#4611) 2025-01-29 10:21:40 +08:00
Kevin Wan
ae09d0e56d chore: use logc instead of logx if possible (#4610) 2025-01-29 00:32:21 +08:00
Kevin Wan
0bc4206d08 feat: support freebsd (#4609) 2025-01-28 11:11:49 +08:00
Kevin Wan
39ce17bfd2 feat: auto validate config (#4607) 2025-01-28 09:37:27 +08:00
Kevin Wan
d415ba39e2 feat: support http->http in gateway (#4605) 2025-01-27 20:00:58 +08:00
Kevin Wan
c71829c8de Potential fix for code scanning alert no. 57: Arbitrary file access during archive extraction ("Zip Slip") (#4604)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-01-27 03:53:35 +00:00
Kevin Wan
a32f6d7642 chore: add more tests for logx/logc (#4603) 2025-01-26 00:07:19 +08:00
Devin
64e8c94198 add Debugfn and Infofn to logx/logc #4595 (#4598) 2025-01-25 22:21:50 +08:00
dependabot[bot]
7d05a4bc93 chore(deps): bump google.golang.org/protobuf from 1.36.3 to 1.36.4 (#4601)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 19:06:31 +08:00
Kevin Wan
44504e8df7 fix: different concreate types still cause panic (#4597) 2025-01-25 18:53:32 +08:00
dependabot[bot]
114311e51b chore(deps): bump google.golang.org/protobuf from 1.36.3 to 1.36.4 in /tools/goctl (#4602)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 18:19:32 +08:00
Kevin Wan
4307ce45fc chore: add redis test (#4593) 2025-01-23 00:03:49 +08:00
xujb
37b54d1fc7 A new User property has been added to the RedisConf object. (#4559) 2025-01-22 23:28:14 +08:00
Kevin Wan
00e0db5def chore: remove useless code (#4592) 2025-01-22 22:55:52 +08:00
Devin
cbcacf31c1 fix: httpx.ParseJsonBody error when request has []byte field #4450 (#4471) 2025-01-22 21:35:32 +08:00
benben
238c92aaa9 fix: wrong way of Unmarshal (#4397) 2025-01-22 13:25:35 +00:00
Kevin Wan
520d2a2075 chore: only upload metrics on mysql (#4591) 2025-01-22 20:26:24 +08:00
Kevin Wan
1023800b02 fix: health check problem (#4590) 2025-01-22 19:32:28 +08:00
MarkJoyMa
030c859171 feat/sqlx_metric (#4587)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
2025-01-22 07:42:02 +00:00
Kevin Wan
e6d1b47a43 Revert "feat/conf_map_required" (#4580) 2025-01-22 07:10:47 +00:00
Devin
6138f85470 Implement #4442 , goctl generate unit test files for api handler and logic (#4443) 2025-01-22 06:52:49 +00:00
Kevin Wan
bf883101d7 fix: etcd discovery mechanism on grpc with idle manager (#4589) 2025-01-22 14:01:18 +08:00
Nanosk07
33011c7ed1 fix: routinegroup & etcd watch goroutine leak (#4514)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2025-01-22 13:38:56 +08:00
saury
17d98f69e0 fix memory leak of grpc resolver (#4490)
Co-authored-by: nk <kui.niu@akuvox.com>
2025-01-22 13:36:13 +08:00
kesonan
b650c8c425 fix syntax of the key expression (#4586) 2025-01-18 15:46:24 +00:00
dependabot[bot]
3d931d7030 chore(deps): bump google.golang.org/protobuf from 1.36.2 to 1.36.3 (#4574)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 14:39:27 +08:00
dependabot[bot]
68da9ed51a chore(deps): bump google.golang.org/protobuf from 1.36.2 to 1.36.3 in /tools/goctl (#4575)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 13:32:33 +08:00
Kevin Wan
b25c45b352 chore: code format (#4572) 2025-01-14 23:16:13 +08:00
R1aEnK
f05234a967 opt:Error message for optimizing mapping data method (#4570) 2025-01-14 22:50:49 +08:00
MarkJoyMa
12071d17b4 feat/conf_map_required (#4405)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
2025-01-12 17:13:41 +00:00
Kevin Wan
11c47d23df fix: ignore empty form values in http request (#4542) 2025-01-13 01:03:32 +08:00
dependabot[bot]
024f285f86 chore(deps): bump go.mongodb.org/mongo-driver from 1.17.1 to 1.17.2 (#4561)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 16:16:34 +08:00
dependabot[bot]
fa4674611a chore(deps): bump google.golang.org/protobuf from 1.36.1 to 1.36.2 (#4556)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-10 08:50:53 +08:00
dependabot[bot]
730c3c5246 chore(deps): bump golang.org/x/net from 0.33.0 to 0.34.0 (#4554)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-10 08:41:10 +08:00
dependabot[bot]
2c9310ac3a chore(deps): bump google.golang.org/protobuf from 1.36.1 to 1.36.2 in /tools/goctl (#4553)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 22:57:51 +08:00
dependabot[bot]
74ba0bcd50 chore(deps): bump golang.org/x/sys from 0.28.0 to 0.29.0 (#4548)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 11:50:51 +08:00
dependabot[bot]
5f4190b6c6 chore(deps): bump golang.org/x/time from 0.8.0 to 0.9.0 (#4547)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 11:38:26 +08:00
Kevin Wan
e1787b4ccb chore: update go version (#4540)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-01-04 12:17:10 +08:00
Kevin Wan
4ac8b492ef chore: update go-zero version (#4539) 2025-01-02 22:33:19 +08:00
Kevin Wan
cdd068575c fix: goctl compile error on windows (#4538) 2025-01-02 22:12:10 +08:00
Kevin Wan
e89e2d8a75 fix compiling error on windows. 2025-01-02 07:00:42 +00:00
Kevin Wan
acd2b94bd9 fix compiling error for goctl on windows 2025-01-02 06:51:40 +00:00
Kevin Wan
6a0c8047f4 fix compiling error for goctl on windows 2025-01-02 06:22:26 +00:00
Kevin Wan
cfe03ea9e1 chore: update goctl deps (#4535) 2025-01-02 00:26:08 +08:00
Kevin Wan
48d21ef8ad chore: add comments (#4534) 2025-01-01 20:49:45 +08:00
Kevin Wan
28a001c5f9 chore: refactor shutdown config, to prevent setting zero values (#4533)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-01-01 20:46:51 +08:00
Qiying Wang
22a41cacc7 feat: add ProcConf to make SetTimeToForceQuit configurable (#4446) 2025-01-01 11:48:53 +00:00
Kevin Wan
fcc246933c fix: service group not working well when callback takes long time (#4531)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2025-01-01 07:06:50 +00:00
dependabot[bot]
5c3679ffe7 chore(deps): bump google.golang.org/protobuf from 1.36.0 to 1.36.1 in /tools/goctl (#4521) 2024-12-28 12:05:03 +08:00
Kevin Wan
eaa01ccb9f Update readme-cn.md (#4525) 2024-12-28 09:59:00 +08:00
dependabot[bot]
b8206fb46a chore(deps): bump google.golang.org/protobuf from 1.36.0 to 1.36.1 (#4523)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-24 09:58:03 +08:00
kesonan
1c3876810e fix command goctl bug invalid (#4520) 2024-12-23 16:00:50 +00:00
Kevin Wan
1d9159ea39 feat: support form array in three notations (#4498)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2024-12-23 00:56:20 +08:00
Kevin Wan
2159d112c3 chore: format the code (#4518) 2024-12-22 15:10:44 +08:00
不插电
f57874a51f fix: DetailedLog format. (#4467) 2024-12-21 03:48:27 +00:00
Xingchen Wang
8625864d43 bugfix:SetSlowThreshold not effective in function logDetails (#4511)
Co-authored-by: Star Wang <starwang@Star-Wangs-MacBook-Pro.local>
2024-12-21 03:22:54 +00:00
dependabot[bot]
8f9ba3ec11 chore(deps): bump golang.org/x/net from 0.32.0 to 0.33.0 (#4516)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-20 12:04:16 +08:00
dependabot[bot]
a1d9bc08f0 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.33.0 to 2.34.0 (#4509)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 18:13:12 +08:00
dependabot[bot]
b9d7f1cc77 chore(deps): bump github.com/emicklei/proto from 1.13.4 to 1.14.0 in /tools/goctl (#4510)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 17:54:44 +08:00
kesonan
6700910f64 (goctl)fix: api timeout limited during api generation (#4513) 2024-12-19 08:19:10 +00:00
godLei6
9c4ed394a7 fix: go work duplicate prefix get err (#4487) 2024-12-19 01:17:50 +00:00
dependabot[bot]
fd07a9c6e4 chore(deps): bump google.golang.org/protobuf from 1.35.1 to 1.36.0 in /tools/goctl (#4504)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 16:01:36 +08:00
dependabot[bot]
b8c239630c chore(deps): bump github.com/emicklei/proto from 1.13.2 to 1.13.4 in /tools/goctl (#4505)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 15:30:04 +08:00
dependabot[bot]
672ea55736 chore(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 in /tools/goctl (#4506)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 15:16:00 +08:00
dependabot[bot]
f7097866bf chore(deps): bump github.com/zeromicro/go-zero from 1.7.3 to 1.7.4 in /tools/goctl (#4507)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 11:27:55 +08:00
dependabot[bot]
796b2bd1b0 chore(deps): bump golang.org/x/crypto from 0.30.0 to 0.31.0 in the go_modules group (#4501)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 00:37:32 +08:00
dependabot[bot]
e1e5fb2071 chore(deps): bump golang.org/x/crypto from 0.28.0 to 0.31.0 in /tools/goctl in the go_modules group (#4502)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 17:20:57 +08:00
kesonan
89ecb50005 remove string restriction on atserver (#4499) 2024-12-17 03:59:30 +00:00
dependabot[bot]
dbed1ea042 chore(deps): bump google.golang.org/protobuf from 1.35.2 to 1.36.0 (#4500) 2024-12-17 11:58:22 +08:00
Kevin Wan
ad291daf78 chore: format Dockerfile template (#4496) 2024-12-14 12:38:33 +08:00
lascyb
13746a3706 Update docker.tpl (#4495) 2024-12-13 16:08:38 +00:00
dependabot[bot]
f03b13f632 chore(deps): bump github.com/fullstorydev/grpcurl from 1.9.1 to 1.9.2 (#4473)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-08 10:27:42 +08:00
dependabot[bot]
f6f64b1286 chore(deps): bump golang.org/x/net from 0.31.0 to 0.32.0 (#4481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-08 10:06:05 +08:00
Kevin Wan
300a415f5d Add go-zero users (#4475) 2024-12-05 00:09:44 +08:00
dependabot[bot]
c5de546f8a chore(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#4470)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 21:46:28 +08:00
Robin
cad243905f fix ts request cli (#4461)
Co-authored-by: robinzhang <azhangrongbing@163.com>
2024-11-21 21:40:08 +08:00
dependabot[bot]
7c8f41d577 chore(deps): bump codecov/codecov-action from 4 to 5 (#4459)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 21:25:53 +08:00
dependabot[bot]
cbd118d55f chore(deps): bump google.golang.org/protobuf from 1.35.1 to 1.35.2 (#4457) 2024-11-15 17:00:10 +08:00
dependabot[bot]
9d2a1b8b0a chore(deps): bump golang.org/x/time from 0.7.0 to 0.8.0 (#4454)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 00:24:06 +08:00
dependabot[bot]
f6ada979aa chore(deps): bump golang.org/x/net from 0.30.0 to 0.31.0 (#4452)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 00:13:28 +08:00
Kevin Wan
53a74759a5 feat: support json array in request body (#4444) 2024-11-05 14:09:30 +00:00
dependabot[bot]
1940f7bd58 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#4447) 2024-11-05 19:37:32 +08:00
Kevin Wan
18cb3141ba feat: support query array in httpx.Parse (#4440) 2024-11-02 13:55:37 +00:00
dependabot[bot]
f822c9a94f chore(deps): bump github.com/fatih/color from 1.17.0 to 1.18.0 (#4434)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-26 14:49:56 +08:00
Kevin Wan
1a3dc75874 chore: upgrade goctl version (#4429) 2024-10-20 10:38:04 +08:00
yangjinheng
796dd5b6e2 fix the source code directory after the soft link (#4425) 2024-10-19 15:37:44 +00:00
dependabot[bot]
94e476ade7 chore(deps): bump github.com/redis/go-redis/v9 from 9.6.1 to 9.7.0 (#4426) 2024-10-19 08:10:32 +08:00
dependabot[bot]
2a74996e1b chore(deps): bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5 (#4424) 2024-10-18 11:38:41 +08:00
fishJack01
f52af1ebf9 feat:New redis method TxPipeline (#4417)
Co-authored-by: fish <fish@fishdeMac-mini.local>
2024-10-13 05:51:55 +00:00
dependabot[bot]
24450f18bb chore(deps): bump google.golang.org/protobuf from 1.34.2 to 1.35.1 (#4414)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-12 21:13:32 +08:00
dependabot[bot]
f1a45d8a23 chore(deps): bump golang.org/x/time from 0.6.0 to 0.7.0 (#4409)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 12:58:06 +08:00
MarkJoyMa
9aebba1566 fix/conf_multi_layer_map (#4407)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
2024-10-05 04:46:32 +00:00
dependabot[bot]
4998479f9a chore(deps): bump golang.org/x/net from 0.29.0 to 0.30.0 (#4410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 12:27:34 +08:00
dependabot[bot]
873d1351ee chore(deps): bump golang.org/x/sys from 0.25.0 to 0.26.0 (#4408)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 12:04:39 +08:00
dependabot[bot]
afcbca8f24 chore(deps): bump go.mongodb.org/mongo-driver from 1.17.0 to 1.17.1 (#4402)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-03 11:16:00 +08:00
dependabot[bot]
5b8126c2cf chore(deps): bump go.uber.org/automaxprocs from 1.5.3 to 1.6.0 (#4389) 2024-09-24 09:16:20 +08:00
Kevin Wan
4dfaf35151 fix: goctl k8s autoscaling version upgrade (#4387) 2024-09-20 23:16:38 +08:00
dependabot[bot]
00cd77c92b chore(deps): bump github.com/prometheus/client_golang from 1.20.3 to 1.20.4 (#4380)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 10:26:52 +08:00
dependabot[bot]
2145a7a93c chore(deps): bump go.mongodb.org/mongo-driver from 1.16.1 to 1.17.0 (#4379)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 22:10:29 +08:00
dependabot[bot]
d5302f2dbe chore(deps): bump golang.org/x/net from 0.28.0 to 0.29.0 (#4360)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-07 21:13:42 +08:00
dependabot[bot]
6181594bc8 chore(deps): bump github.com/jhump/protoreflect from 1.16.0 to 1.17.0 (#4359)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 20:13:29 +08:00
dependabot[bot]
11c10e51ff chore(deps): bump github.com/prometheus/client_golang from 1.20.2 to 1.20.3 (#4361)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 18:52:59 +08:00
Kevin Wan
3f03126d27 chore: fix goctl Dockerfile warnings (#4358) 2024-09-05 22:13:01 +08:00
dependabot[bot]
d43adc2823 chore(deps): bump golang.org/x/sys from 0.24.0 to 0.25.0 (#4356)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-05 10:17:27 +08:00
Kevin Wan
656222b572 chore: update goctl version (#4353) 2024-09-03 09:22:04 +08:00
Kevin Wan
077b6072fa chore: coding style (#4352) 2024-09-03 09:06:50 +08:00
jursonmo
0cafb1164b should check if devServer Enabled, then call once.Do() (#4351)
Co-authored-by: william <myname@example.com>
2024-09-03 00:54:43 +00:00
MarkJoyMa
90afa08367 Revert "fix: etcd scheme on grpc resolver" (#4349) 2024-09-03 00:48:08 +00:00
Kevin Wan
c92f788292 chore: update go version in test (#4347) 2024-09-01 16:19:31 +08:00
Kevin Wan
e94be9b302 chore: update go version for building goctl releases (#4346) 2024-09-01 16:02:49 +08:00
Kevin Wan
e713d9013d chore: update goctl deps (#4345) 2024-09-01 15:42:45 +08:00
Kevin Wan
24d6150073 chore: refactor config center (#4339)
Signed-off-by: kevin <wanjunfeng@gmail.com>
2024-08-28 12:02:48 +00:00
MarkJoyMa
44cddec5c3 feat: added configuration center function (#3035)
Co-authored-by: aiden.ma <Aiden.ma@yijinin.com>
2024-08-28 14:47:52 +08:00
Kevin Wan
47d13e5ef8 fix: etcd scheme on grpc resolver (#4121) 2024-08-27 23:13:37 +08:00
Kevin Wan
896e1a2abb chore: refactor logx file time format (#4335) 2024-08-27 22:01:01 +08:00
kui
075817a8dd Add custom log file name date format (#4333) 2024-08-27 20:43:25 +08:00
dependabot[bot]
29400f6814 chore(deps): bump github.com/prometheus/client_golang from 1.20.1 to 1.20.2 (#4334)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-27 19:51:54 +08:00
dependabot[bot]
34f536264f chore(deps): bump github.com/prometheus/client_golang from 1.20.0 to 1.20.1 (#4324)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-21 17:38:54 +08:00
Kevin Wan
9d9c7e0fe0 feat: support build tag to reduce binary size w/o k8s (#4323) 2024-08-20 19:53:20 +08:00
dependabot[bot]
e220d3a4cb chore(deps): bump github.com/prometheus/client_golang from 1.19.1 to 1.20.0 (#4317)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 12:22:06 +08:00
dependabot[bot]
193dcf90bc chore(deps): bump golang.org/x/sys from 0.23.0 to 0.24.0 (#4307)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-10 21:15:08 +08:00
featherlight
03756c9166 refactor zrpc server interceptor builder (#4300) 2024-08-08 14:37:19 +00:00
kesonan
c1f12c5784 (goctl): fix map conversion (#4306) 2024-08-08 14:19:14 +00:00
dependabot[bot]
2883111af5 chore(deps): bump go.mongodb.org/mongo-driver from 1.16.0 to 1.16.1 (#4305)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 22:07:18 +08:00
dependabot[bot]
2758c4e842 chore(deps): bump golang.org/x/net from 0.27.0 to 0.28.0 (#4301)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-07 20:19:47 +08:00
Kevin Wan
4196ddb3e3 Update readme-cn.md (#4294) 2024-08-06 17:54:38 +08:00
dependabot[bot]
e24d797226 chore(deps): bump golang.org/x/time from 0.5.0 to 0.6.0 (#4299)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 17:54:01 +08:00
dependabot[bot]
d4349fa958 chore(deps): bump golang.org/x/sys from 0.22.0 to 0.23.0 (#4298)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-06 16:39:57 +08:00
kesonan
da2c14d45f (goctl): fix quickstart error while reading go module info (#4297) 2024-08-05 15:29:06 +00:00
kesonan
64e3aeda55 Add goctl version to code header (#4293) 2024-08-03 14:22:51 +00:00
Kevin Wan
dedba17219 refactor: simplify BatchError (#4292) 2024-08-03 13:57:41 +08:00
kesonan
c6348b9855 refactor goctl-compare (#4290) 2024-08-03 04:26:24 +00:00
chentong
8689a6247e refactor(core/errorx): use errors.Join simplify error handle (#4289) 2024-08-03 03:00:59 +00:00
Rui Chen
ff6ee25d23 ci: update workflows to use go.mod instead of specifying go version (#4286)
Signed-off-by: Rui Chen <rui@chenrui.dev>
2024-08-03 02:12:28 +00:00
Kevin Wan
5213243bbb Update readme.md (#4287) 2024-08-02 18:26:54 +08:00
Kevin Wan
2588a36555 feat: support rest.WithCorsHeaders to customize cors headers (#4284) 2024-07-30 17:29:44 +08:00
Krystian Kulas
c2421beb25 fix: readme remove duplicate text (#4280) 2024-07-29 01:06:29 +00:00
kesonan
dfe8a81c76 Upgrade goctl version to 1.7.1 (#4282) 2024-07-29 00:54:21 +00:00
kesonan
ee643a945e (goctl): fix nested struct generation (#4281) 2024-07-28 15:40:25 +00:00
Kevin Wan
eeda6efae7 chore: upgrade go-zero version (#4277) 2024-07-27 17:31:32 +08:00
291 changed files with 27895 additions and 2496 deletions

View File

@@ -17,7 +17,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
go-version: '1.20'
go-version-file: go.mod
check-latest: true
cache: true
id: go
@@ -40,7 +40,7 @@ jobs:
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
test-win:
name: Windows
@@ -52,8 +52,8 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
# use 1.20 to guarantee Go 1.20 compatibility
go-version: '1.20'
# make sure Go version compatible with go-zero
go-version-file: go.mod
check-latest: true
cache: true

View File

@@ -1,18 +0,0 @@
name: 'issue-translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: true
# not require, default false, . Decide whether to modify the issue title
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿
# not require. Customize the translation robot prefix message.

View File

@@ -22,7 +22,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.19.13.linux-amd64.tar.gz"
goversion: "https://dl.google.com/go/go1.21.13.linux-amd64.tar.gz"
project_path: "tools/goctl"
binary_name: "goctl"
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md

View File

@@ -14,6 +14,6 @@ jobs:
# Report all results.
filter_mode: nofilter
# Exit with 1 when it find at least one finding.
fail_on_error: true
fail_level: any
# Set staticcheck flags
staticcheck_flags: -checks=inherit,-SA1019,-SA1029,-SA5008

42
.github/workflows/version-check.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Release Version Check
on:
push:
tags:
- 'tools/goctl/v*'
workflow_dispatch:
jobs:
version-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Extract tag version
id: get_version
run: |
# Extract version from tools/goctl/v* format
VERSION="${GITHUB_REF#refs/tags/tools/goctl/v}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Extracted version: $VERSION"
- name: Check version in goctl source code
run: |
# Change to goctl directory
cd tools/goctl
# Check version in BuildVersion constant
VERSION_IN_CODE=$(grep -r "const BuildVersion =" . | grep -o '".*"' | tr -d '"')
echo "Version in code: $VERSION_IN_CODE"
echo "Expected version: $VERSION"
if [ "$VERSION_IN_CODE" != "$VERSION" ]; then
echo "Version mismatch: Version in code ($VERSION_IN_CODE) doesn't match tag version ($VERSION)"
exit 1
fi
echo "✅ Version check passed!"

View File

@@ -8,16 +8,12 @@ import (
"sync"
"time"
"github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/stringx"
)
const (
numHistoryReasons = 5
timeFormat = "15:04:05"
)
const numHistoryReasons = 5
// ErrServiceUnavailable is returned when the Breaker state is open.
var ErrServiceUnavailable = errors.New("circuit breaker is open")
@@ -262,9 +258,9 @@ type errorWindow struct {
func (ew *errorWindow) add(reason string) {
ew.lock.Lock()
ew.reasons[ew.index] = fmt.Sprintf("%s %s", time.Now().Format(timeFormat), reason)
ew.reasons[ew.index] = fmt.Sprintf("%s %s", time.Now().Format(time.TimeOnly), reason)
ew.index = (ew.index + 1) % numHistoryReasons
ew.count = mathx.MinInt(ew.count+1, numHistoryReasons)
ew.count = min(ew.count+1, numHistoryReasons)
ew.lock.Unlock()
}

View File

@@ -11,9 +11,9 @@ import (
func TestNopBreaker(t *testing.T) {
b := NopBreaker()
assert.Equal(t, nopBreakerName, b.Name())
p, err := b.Allow()
_, err := b.Allow()
assert.Nil(t, err)
p, err = b.AllowCtx(context.Background())
p, err := b.AllowCtx(context.Background())
assert.Nil(t, err)
p.Accept()
for i := 0; i < 1000; i++ {

View File

@@ -62,7 +62,11 @@ func Load(file string, v any, opts ...Option) error {
return loader([]byte(os.ExpandEnv(string(content))), v)
}
return loader(content, v)
if err = loader(content, v); err != nil {
return err
}
return validate(v)
}
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
@@ -85,7 +89,12 @@ func LoadFromJsonBytes(content []byte, v any) error {
lowerCaseKeyMap := toLowerCaseKeyMap(m, info)
return mapping.UnmarshalJsonMap(lowerCaseKeyMap, v, mapping.WithCanonicalKeyFunc(toLowerCase))
if err = mapping.UnmarshalJsonMap(lowerCaseKeyMap, v,
mapping.WithCanonicalKeyFunc(toLowerCase)); err != nil {
return err
}
return validate(v)
}
// LoadConfigFromJsonBytes loads config into v from content json bytes.
@@ -189,10 +198,10 @@ func buildFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) {
switch tp.Kind() {
case reflect.Struct:
return buildStructFieldsInfo(tp, fullName)
case reflect.Array, reflect.Slice:
case reflect.Array, reflect.Slice, reflect.Map:
return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName)
case reflect.Chan, reflect.Func:
return nil, fmt.Errorf("unsupported type: %s", tp.Kind())
return nil, fmt.Errorf("unsupported type: %s, fullName: %s", tp.Kind(), fullName)
default:
return &fieldInfo{
children: make(map[string]*fieldInfo),
@@ -332,6 +341,8 @@ func toLowerCaseKeyMap(m map[string]any, info *fieldInfo) map[string]any {
res[lk] = toLowerCaseInterface(v, ti)
} else if info.mapField != nil {
res[k] = toLowerCaseInterface(v, info.mapField)
} else if vv, ok := v.(map[string]any); ok {
res[k] = toLowerCaseKeyMap(vv, info)
} else {
res[k] = v
}

View File

@@ -1,6 +1,7 @@
package conf
import (
"errors"
"os"
"reflect"
"testing"
@@ -40,9 +41,8 @@ func TestConfigJson(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test, func(t *testing.T) {
tmpfile, err := createTempFile(test, text)
tmpfile, err := createTempFile(t, test, text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
@@ -82,9 +82,8 @@ c = "${FOO}"
d = "abcd!@#$112"
`
t.Setenv("FOO", "2")
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
@@ -105,9 +104,8 @@ b = 1
c = "FOO"
d = "abcd"
`
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
@@ -127,9 +125,8 @@ func TestConfigWithLower(t *testing.T) {
text := `a = "foo"
b = 1
`
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
@@ -207,9 +204,8 @@ c = "${FOO}"
d = "abcd!@#112"
`
t.Setenv("FOO", "2")
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
@@ -241,9 +237,8 @@ func TestConfigJsonEnv(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test, func(t *testing.T) {
tmpfile, err := createTempFile(test, text)
tmpfile, err := createTempFile(t, test, text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
@@ -1192,6 +1187,42 @@ Email = "bar"`)
assert.Len(t, c.Value, 2)
}
})
t.Run("multi layer map", func(t *testing.T) {
type Value struct {
User struct {
Name string
}
}
type Config struct {
Value map[string]map[string]Value
}
var input = []byte(`
[Value.first.User1.User]
Name = "foo"
[Value.second.User2.User]
Name = "bar"
`)
var c Config
if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
assert.Len(t, c.Value, 2)
}
})
}
func Test_LoadBadConfig(t *testing.T) {
type Config struct {
Name string `json:"name,options=foo|bar"`
}
file, err := createTempFile(t, ".json", `{"name": "baz"}`)
assert.NoError(t, err)
var c Config
err = Load(file, &c)
assert.Error(t, err)
}
func Test_getFullName(t *testing.T) {
@@ -1199,6 +1230,26 @@ func Test_getFullName(t *testing.T) {
assert.Equal(t, "a", getFullName("", "a"))
}
func TestValidate(t *testing.T) {
t.Run("normal config", func(t *testing.T) {
var c mockConfig
err := LoadFromJsonBytes([]byte(`{"val": "hello", "number": 8}`), &c)
assert.NoError(t, err)
})
t.Run("error no int", func(t *testing.T) {
var c mockConfig
err := LoadFromJsonBytes([]byte(`{"val": "hello"}`), &c)
assert.Error(t, err)
})
t.Run("error no string", func(t *testing.T) {
var c mockConfig
err := LoadFromJsonBytes([]byte(`{"number": 8}`), &c)
assert.Error(t, err)
})
}
func Test_buildFieldsInfo(t *testing.T) {
type ParentSt struct {
Name string
@@ -1288,13 +1339,13 @@ func Test_buildFieldsInfo(t *testing.T) {
}
}
func createTempFile(ext, text string) (string, error) {
func createTempFile(t *testing.T, ext, text string) (string, error) {
tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
if err != nil {
return "", err
}
if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
if err = os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
return "", err
}
@@ -1303,5 +1354,26 @@ func createTempFile(ext, text string) (string, error) {
return "", err
}
t.Cleanup(func() {
_ = os.Remove(filename)
})
return filename, nil
}
type mockConfig struct {
Val string
Number int
}
func (m mockConfig) Validate() error {
if len(m.Val) == 0 {
return errors.New("val is empty")
}
if m.Number == 0 {
return errors.New("number is zero")
}
return nil
}

12
core/conf/validate.go Normal file
View File

@@ -0,0 +1,12 @@
package conf
import "github.com/zeromicro/go-zero/core/validation"
// validate validates the value if it implements the Validator interface.
func validate(v any) error {
if val, ok := v.(validation.Validator); ok {
return val.Validate()
}
return nil
}

View File

@@ -0,0 +1,81 @@
package conf
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
type mockType int
func (m mockType) Validate() error {
if m < 10 {
return errors.New("invalid value")
}
return nil
}
type anotherMockType int
func Test_validate(t *testing.T) {
tests := []struct {
name string
v any
wantErr bool
}{
{
name: "invalid",
v: mockType(5),
wantErr: true,
},
{
name: "valid",
v: mockType(10),
wantErr: false,
},
{
name: "not validator",
v: anotherMockType(5),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validate(tt.v)
assert.Equal(t, tt.wantErr, err != nil)
})
}
}
type mockVal struct {
}
func (m mockVal) Validate() error {
return errors.New("invalid value")
}
func Test_validateValPtr(t *testing.T) {
tests := []struct {
name string
v any
wantErr bool
}{
{
name: "invalid",
v: mockVal{},
},
{
name: "invalid value",
v: &mockVal{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Error(t, validate(tt.v))
})
}
}

View File

@@ -0,0 +1,200 @@
package configurator
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"sync/atomic"
"github.com/zeromicro/go-zero/core/configcenter/subscriber"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mapping"
"github.com/zeromicro/go-zero/core/threading"
)
var (
errEmptyConfig = errors.New("empty config value")
errMissingUnmarshalerType = errors.New("missing unmarshaler type")
)
// Configurator is the interface for configuration center.
type Configurator[T any] interface {
// GetConfig returns the subscription value.
GetConfig() (T, error)
// AddListener adds a listener to the subscriber.
AddListener(listener func())
}
type (
// Config is the configuration for Configurator.
Config struct {
// Type is the value type, yaml, json or toml.
Type string `json:",default=yaml,options=[yaml,json,toml]"`
// Log is the flag to control logging.
Log bool `json:",default=true"`
}
configCenter[T any] struct {
conf Config
unmarshaler LoaderFn
subscriber subscriber.Subscriber
listeners []func()
lock sync.Mutex
snapshot atomic.Value
}
value[T any] struct {
data string
marshalData T
err error
}
)
// Configurator is the interface for configuration center.
var _ Configurator[any] = (*configCenter[any])(nil)
// MustNewConfigCenter returns a Configurator, exits on errors.
func MustNewConfigCenter[T any](c Config, subscriber subscriber.Subscriber) Configurator[T] {
cc, err := NewConfigCenter[T](c, subscriber)
logx.Must(err)
return cc
}
// NewConfigCenter returns a Configurator.
func NewConfigCenter[T any](c Config, subscriber subscriber.Subscriber) (Configurator[T], error) {
unmarshaler, ok := Unmarshaler(strings.ToLower(c.Type))
if !ok {
return nil, fmt.Errorf("unknown format: %s", c.Type)
}
cc := &configCenter[T]{
conf: c,
unmarshaler: unmarshaler,
subscriber: subscriber,
}
if err := cc.loadConfig(); err != nil {
return nil, err
}
if err := cc.subscriber.AddListener(cc.onChange); err != nil {
return nil, err
}
if _, err := cc.GetConfig(); err != nil {
return nil, err
}
return cc, nil
}
// AddListener adds listener to s.
func (c *configCenter[T]) AddListener(listener func()) {
c.lock.Lock()
defer c.lock.Unlock()
c.listeners = append(c.listeners, listener)
}
// GetConfig return structured config.
func (c *configCenter[T]) GetConfig() (T, error) {
v := c.value()
if v == nil || len(v.data) == 0 {
var empty T
return empty, errEmptyConfig
}
return v.marshalData, v.err
}
// Value returns the subscription value.
func (c *configCenter[T]) Value() string {
v := c.value()
if v == nil {
return ""
}
return v.data
}
func (c *configCenter[T]) loadConfig() error {
v, err := c.subscriber.Value()
if err != nil {
if c.conf.Log {
logx.Errorf("ConfigCenter loads changed configuration, error: %v", err)
}
return err
}
if c.conf.Log {
logx.Infof("ConfigCenter loads changed configuration, content [%s]", v)
}
c.snapshot.Store(c.genValue(v))
return nil
}
func (c *configCenter[T]) onChange() {
if err := c.loadConfig(); err != nil {
return
}
c.lock.Lock()
listeners := make([]func(), len(c.listeners))
copy(listeners, c.listeners)
c.lock.Unlock()
for _, l := range listeners {
threading.GoSafe(l)
}
}
func (c *configCenter[T]) value() *value[T] {
content := c.snapshot.Load()
if content == nil {
return nil
}
return content.(*value[T])
}
func (c *configCenter[T]) genValue(data string) *value[T] {
v := &value[T]{
data: data,
}
if len(data) == 0 {
return v
}
t := reflect.TypeOf(v.marshalData)
// if the type is nil, it means that the user has not set the type of the configuration.
if t == nil {
v.err = errMissingUnmarshalerType
return v
}
t = mapping.Deref(t)
switch t.Kind() {
case reflect.Struct, reflect.Array, reflect.Slice:
if err := c.unmarshaler([]byte(data), &v.marshalData); err != nil {
v.err = err
if c.conf.Log {
logx.Errorf("ConfigCenter unmarshal configuration failed, err: %+v, content [%s]",
err.Error(), data)
}
}
case reflect.String:
if str, ok := any(data).(T); ok {
v.marshalData = str
} else {
v.err = errMissingUnmarshalerType
}
default:
if c.conf.Log {
logx.Errorf("ConfigCenter unmarshal configuration missing unmarshaler for type: %s, content [%s]",
t.Kind(), data)
}
v.err = errMissingUnmarshalerType
}
return v
}

View File

@@ -0,0 +1,233 @@
package configurator
import (
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewConfigCenter(t *testing.T) {
_, err := NewConfigCenter[any](Config{
Log: true,
}, &mockSubscriber{})
assert.Error(t, err)
_, err = NewConfigCenter[any](Config{
Type: "json",
Log: true,
}, &mockSubscriber{})
assert.Error(t, err)
}
func TestConfigCenter_GetConfig(t *testing.T) {
mock := &mockSubscriber{}
type Data struct {
Name string `json:"name"`
}
mock.v = `{"name": "go-zero"}`
c1, err := NewConfigCenter[Data](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
data, err := c1.GetConfig()
assert.NoError(t, err)
assert.Equal(t, "go-zero", data.Name)
mock.v = `{"name": "111"}`
c2, err := NewConfigCenter[Data](Config{Type: "json"}, mock)
assert.NoError(t, err)
mock.v = `{}`
c3, err := NewConfigCenter[string](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
_, err = c3.GetConfig()
assert.NoError(t, err)
data, err = c2.GetConfig()
assert.NoError(t, err)
mock.lisErr = errors.New("mock error")
_, err = NewConfigCenter[Data](Config{
Type: "json",
Log: true,
}, mock)
assert.Error(t, err)
}
func TestConfigCenter_onChange(t *testing.T) {
mock := &mockSubscriber{}
type Data struct {
Name string `json:"name"`
}
mock.v = `{"name": "go-zero"}`
c1, err := NewConfigCenter[Data](Config{Type: "json", Log: true}, mock)
assert.NoError(t, err)
data, err := c1.GetConfig()
assert.NoError(t, err)
assert.Equal(t, "go-zero", data.Name)
mock.v = `{"name": "go-zero2"}`
mock.change()
data, err = c1.GetConfig()
assert.NoError(t, err)
assert.Equal(t, "go-zero2", data.Name)
mock.valErr = errors.New("mock error")
_, err = NewConfigCenter[Data](Config{Type: "json", Log: false}, mock)
assert.Error(t, err)
}
func TestConfigCenter_Value(t *testing.T) {
mock := &mockSubscriber{}
mock.v = "1234"
c, err := NewConfigCenter[string](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
cc := c.(*configCenter[string])
assert.Equal(t, cc.Value(), "1234")
mock.valErr = errors.New("mock error")
_, err = NewConfigCenter[any](Config{
Type: "json",
Log: true,
}, mock)
assert.Error(t, err)
}
func TestConfigCenter_AddListener(t *testing.T) {
mock := &mockSubscriber{}
mock.v = "1234"
c, err := NewConfigCenter[string](Config{
Type: "json",
Log: true,
}, mock)
assert.NoError(t, err)
cc := c.(*configCenter[string])
var a, b int
var mutex sync.Mutex
cc.AddListener(func() {
mutex.Lock()
a = 1
mutex.Unlock()
})
cc.AddListener(func() {
mutex.Lock()
b = 2
mutex.Unlock()
})
assert.Equal(t, 2, len(cc.listeners))
mock.change()
time.Sleep(time.Millisecond * 100)
mutex.Lock()
assert.Equal(t, 1, a)
assert.Equal(t, 2, b)
mutex.Unlock()
}
func TestConfigCenter_genValue(t *testing.T) {
t.Run("data is empty", func(t *testing.T) {
c := &configCenter[string]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("")
assert.Equal(t, "", v.data)
})
t.Run("invalid template type", func(t *testing.T) {
c := &configCenter[any]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("xxxx")
assert.Equal(t, errMissingUnmarshalerType, v.err)
})
t.Run("unsupported template type", func(t *testing.T) {
c := &configCenter[int]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("1")
assert.Equal(t, errMissingUnmarshalerType, v.err)
})
t.Run("supported template string type", func(t *testing.T) {
c := &configCenter[string]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue("12345")
assert.NoError(t, v.err)
assert.Equal(t, "12345", v.data)
})
t.Run("unmarshal fail", func(t *testing.T) {
c := &configCenter[struct {
Name string `json:"name"`
}]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue(`{"name":"new name}`)
assert.Equal(t, `{"name":"new name}`, v.data)
assert.Error(t, v.err)
})
t.Run("success", func(t *testing.T) {
c := &configCenter[struct {
Name string `json:"name"`
}]{
unmarshaler: registry.unmarshalers["json"],
conf: Config{Log: true},
}
v := c.genValue(`{"name":"new name"}`)
assert.Equal(t, `{"name":"new name"}`, v.data)
assert.Equal(t, "new name", v.marshalData.Name)
assert.NoError(t, v.err)
})
}
type mockSubscriber struct {
v string
lisErr, valErr error
listener func()
}
func (m *mockSubscriber) AddListener(listener func()) error {
m.listener = listener
return m.lisErr
}
func (m *mockSubscriber) Value() (string, error) {
return m.v, m.valErr
}
func (m *mockSubscriber) change() {
if m.listener != nil {
m.listener()
}
}

View File

@@ -0,0 +1,67 @@
package subscriber
import (
"github.com/zeromicro/go-zero/core/discov"
"github.com/zeromicro/go-zero/core/logx"
)
type (
// etcdSubscriber is a subscriber that subscribes to etcd.
etcdSubscriber struct {
*discov.Subscriber
}
// EtcdConf is the configuration for etcd.
EtcdConf = discov.EtcdConf
)
// MustNewEtcdSubscriber returns an etcd Subscriber, exits on errors.
func MustNewEtcdSubscriber(conf EtcdConf) Subscriber {
s, err := NewEtcdSubscriber(conf)
logx.Must(err)
return s
}
// NewEtcdSubscriber returns an etcd Subscriber.
func NewEtcdSubscriber(conf EtcdConf) (Subscriber, error) {
opts := buildSubOptions(conf)
s, err := discov.NewSubscriber(conf.Hosts, conf.Key, opts...)
if err != nil {
return nil, err
}
return &etcdSubscriber{Subscriber: s}, nil
}
// buildSubOptions constructs the options for creating a new etcd subscriber.
func buildSubOptions(conf EtcdConf) []discov.SubOption {
opts := []discov.SubOption{
discov.WithExactMatch(),
}
if len(conf.User) > 0 {
opts = append(opts, discov.WithSubEtcdAccount(conf.User, conf.Pass))
}
if len(conf.CertFile) > 0 || len(conf.CertKeyFile) > 0 || len(conf.CACertFile) > 0 {
opts = append(opts, discov.WithSubEtcdTLS(conf.CertFile, conf.CertKeyFile,
conf.CACertFile, conf.InsecureSkipVerify))
}
return opts
}
// AddListener adds a listener to the subscriber.
func (s *etcdSubscriber) AddListener(listener func()) error {
s.Subscriber.AddListener(listener)
return nil
}
// Value returns the value of the subscriber.
func (s *etcdSubscriber) Value() (string, error) {
vs := s.Subscriber.Values()
if len(vs) > 0 {
return vs[len(vs)-1], nil
}
return "", nil
}

View File

@@ -0,0 +1,9 @@
package subscriber
// Subscriber is the interface for configcenter subscribers.
type Subscriber interface {
// AddListener adds a listener to the subscriber.
AddListener(listener func()) error
// Value returns the value of the subscriber.
Value() (string, error)
}

View File

@@ -0,0 +1,41 @@
package configurator
import (
"sync"
"github.com/zeromicro/go-zero/core/conf"
)
var registry = &unmarshalerRegistry{
unmarshalers: map[string]LoaderFn{
"json": conf.LoadFromJsonBytes,
"toml": conf.LoadFromTomlBytes,
"yaml": conf.LoadFromYamlBytes,
},
}
type (
// LoaderFn is the function type for loading configuration.
LoaderFn func([]byte, any) error
// unmarshalerRegistry is the registry for unmarshalers.
unmarshalerRegistry struct {
unmarshalers map[string]LoaderFn
mu sync.RWMutex
}
)
// RegisterUnmarshaler registers an unmarshaler.
func RegisterUnmarshaler(name string, fn LoaderFn) {
registry.mu.Lock()
defer registry.mu.Unlock()
registry.unmarshalers[name] = fn
}
// Unmarshaler returns the unmarshaler by name.
func Unmarshaler(name string) (LoaderFn, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
fn, ok := registry.unmarshalers[name]
return fn, ok
}

View File

@@ -0,0 +1,28 @@
package configurator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRegisterUnmarshaler(t *testing.T) {
RegisterUnmarshaler("test", func(data []byte, v interface{}) error {
return nil
})
_, ok := Unmarshaler("test")
assert.True(t, ok)
_, ok = Unmarshaler("test2")
assert.False(t, ok)
_, ok = Unmarshaler("json")
assert.True(t, ok)
_, ok = Unmarshaler("toml")
assert.True(t, ok)
_, ok = Unmarshaler("yaml")
assert.True(t, ok)
}

View File

@@ -10,21 +10,24 @@ import (
"sync"
"time"
"github.com/zeromicro/go-zero/core/contextx"
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/logc"
"github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/threading"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
clientv3 "go.etcd.io/etcd/client/v3"
)
const coolDownDeviation = 0.05
var (
registry = Registry{
clusters: make(map[string]*cluster),
}
connManager = syncx.NewResourceManager()
errClosed = errors.New("etcd monitor chan has been closed")
connManager = syncx.NewResourceManager()
coolDownUnstable = mathx.NewUnstable(coolDownDeviation)
errClosed = errors.New("etcd monitor chan has been closed")
)
// A Registry is a registry that manages the etcd client connections.
@@ -40,33 +43,92 @@ func GetRegistry() *Registry {
// GetConn returns an etcd client connection associated with given endpoints.
func (r *Registry) GetConn(endpoints []string) (EtcdClient, error) {
c, _ := r.getCluster(endpoints)
c, _ := r.getOrCreateCluster(endpoints)
return c.getClient()
}
// Monitor monitors the key on given etcd endpoints, notify with the given UpdateListener.
func (r *Registry) Monitor(endpoints []string, key string, l UpdateListener) error {
c, exists := r.getCluster(endpoints)
func (r *Registry) Monitor(endpoints []string, key string, exactMatch bool, l UpdateListener) error {
wkey := watchKey{
key: key,
exactMatch: exactMatch,
}
c, exists := r.getOrCreateCluster(endpoints)
// if exists, the existing values should be updated to the listener.
if exists {
kvs := c.getCurrent(key)
for _, kv := range kvs {
l.OnAdd(kv)
c.lock.Lock()
watcher, ok := c.watchers[wkey]
if ok {
watcher.listeners = append(watcher.listeners, l)
}
c.lock.Unlock()
if ok {
kvs := c.getCurrent(wkey)
for _, kv := range kvs {
l.OnAdd(kv)
}
return nil
}
}
return c.monitor(key, l)
return c.monitor(wkey, l)
}
func (r *Registry) getCluster(endpoints []string) (c *cluster, exists bool) {
func (r *Registry) Unmonitor(endpoints []string, key string, exactMatch bool, l UpdateListener) {
c, exists := r.getCluster(endpoints)
if !exists {
return
}
wkey := watchKey{
key: key,
exactMatch: exactMatch,
}
c.lock.Lock()
defer c.lock.Unlock()
watcher, ok := c.watchers[wkey]
if !ok {
return
}
for i, listener := range watcher.listeners {
if listener == l {
watcher.listeners = append(watcher.listeners[:i], watcher.listeners[i+1:]...)
break
}
}
if len(watcher.listeners) == 0 {
if watcher.cancel != nil {
watcher.cancel()
}
delete(c.watchers, wkey)
}
}
func (r *Registry) getCluster(endpoints []string) (*cluster, bool) {
clusterKey := getClusterKey(endpoints)
r.lock.RLock()
c, exists = r.clusters[clusterKey]
c, ok := r.clusters[clusterKey]
r.lock.RUnlock()
return c, ok
}
func (r *Registry) getOrCreateCluster(endpoints []string) (c *cluster, exists bool) {
c, exists = r.getCluster(endpoints)
if !exists {
clusterKey := getClusterKey(endpoints)
r.lock.Lock()
defer r.lock.Unlock()
// double-check locking
c, exists = r.clusters[clusterKey]
if !exists {
@@ -78,29 +140,51 @@ func (r *Registry) getCluster(endpoints []string) (c *cluster, exists bool) {
return
}
type cluster struct {
endpoints []string
key string
values map[string]map[string]string
listeners map[string][]UpdateListener
watchGroup *threading.RoutineGroup
done chan lang.PlaceholderType
lock sync.RWMutex
}
type (
watchKey struct {
key string
exactMatch bool
}
watchValue struct {
listeners []UpdateListener
values map[string]string
cancel context.CancelFunc
}
cluster struct {
endpoints []string
key string
watchers map[watchKey]*watchValue
watchGroup *threading.RoutineGroup
done chan lang.PlaceholderType
lock sync.RWMutex
}
)
func newCluster(endpoints []string) *cluster {
return &cluster{
endpoints: endpoints,
key: getClusterKey(endpoints),
values: make(map[string]map[string]string),
listeners: make(map[string][]UpdateListener),
watchers: make(map[watchKey]*watchValue),
watchGroup: threading.NewRoutineGroup(),
done: make(chan lang.PlaceholderType),
}
}
func (c *cluster) context(cli EtcdClient) context.Context {
return contextx.ValueOnlyFrom(cli.Ctx())
func (c *cluster) addListener(key watchKey, l UpdateListener) {
c.lock.Lock()
defer c.lock.Unlock()
watcher, ok := c.watchers[key]
if ok {
watcher.listeners = append(watcher.listeners, l)
return
}
val := newWatchValue()
val.listeners = []UpdateListener{l}
c.watchers[key] = val
}
func (c *cluster) getClient() (EtcdClient, error) {
@@ -114,12 +198,17 @@ func (c *cluster) getClient() (EtcdClient, error) {
return val.(EtcdClient), nil
}
func (c *cluster) getCurrent(key string) []KV {
func (c *cluster) getCurrent(key watchKey) []KV {
c.lock.RLock()
defer c.lock.RUnlock()
watcher, ok := c.watchers[key]
if !ok {
return nil
}
var kvs []KV
for k, v := range c.values[key] {
for k, v := range watcher.values {
kvs = append(kvs, KV{
Key: k,
Val: v,
@@ -129,43 +218,23 @@ func (c *cluster) getCurrent(key string) []KV {
return kvs
}
func (c *cluster) handleChanges(key string, kvs []KV) {
var add []KV
var remove []KV
func (c *cluster) handleChanges(key watchKey, kvs []KV) {
c.lock.Lock()
listeners := append([]UpdateListener(nil), c.listeners[key]...)
vals, ok := c.values[key]
watcher, ok := c.watchers[key]
if !ok {
add = kvs
vals = make(map[string]string)
for _, kv := range kvs {
vals[kv.Key] = kv.Val
}
c.values[key] = vals
} else {
m := make(map[string]string)
for _, kv := range kvs {
m[kv.Key] = kv.Val
}
for k, v := range vals {
if val, ok := m[k]; !ok || v != val {
remove = append(remove, KV{
Key: k,
Val: v,
})
}
}
for k, v := range m {
if val, ok := vals[k]; !ok || v != val {
add = append(add, KV{
Key: k,
Val: v,
})
}
}
c.values[key] = m
c.lock.Unlock()
return
}
listeners := append([]UpdateListener(nil), watcher.listeners...)
// watcher.values cannot be nil
vals := watcher.values
newVals := make(map[string]string, len(kvs)+len(vals))
for _, kv := range kvs {
newVals[kv.Key] = kv.Val
}
add, remove := calculateChanges(vals, newVals)
watcher.values = newVals
c.lock.Unlock()
for _, kv := range add {
@@ -180,20 +249,22 @@ func (c *cluster) handleChanges(key string, kvs []KV) {
}
}
func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
func (c *cluster) handleWatchEvents(ctx context.Context, key watchKey, events []*clientv3.Event) {
c.lock.RLock()
listeners := append([]UpdateListener(nil), c.listeners[key]...)
watcher, ok := c.watchers[key]
if !ok {
c.lock.RUnlock()
return
}
listeners := append([]UpdateListener(nil), watcher.listeners...)
c.lock.RUnlock()
for _, ev := range events {
switch ev.Type {
case clientv3.EventTypePut:
c.lock.Lock()
if vals, ok := c.values[key]; ok {
vals[string(ev.Kv.Key)] = string(ev.Kv.Value)
} else {
c.values[key] = map[string]string{string(ev.Kv.Key): string(ev.Kv.Value)}
}
watcher.values[string(ev.Kv.Key)] = string(ev.Kv.Value)
c.lock.Unlock()
for _, l := range listeners {
l.OnAdd(KV{
@@ -203,9 +274,7 @@ func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
}
case clientv3.EventTypeDelete:
c.lock.Lock()
if vals, ok := c.values[key]; ok {
delete(vals, string(ev.Kv.Key))
}
delete(watcher.values, string(ev.Kv.Key))
c.lock.Unlock()
for _, l := range listeners {
l.OnDelete(KV{
@@ -214,24 +283,29 @@ func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
})
}
default:
logx.Errorf("Unknown event type: %v", ev.Type)
logc.Errorf(ctx, "Unknown event type: %v", ev.Type)
}
}
}
func (c *cluster) load(cli EtcdClient, key string) int64 {
func (c *cluster) load(cli EtcdClient, key watchKey) int64 {
var resp *clientv3.GetResponse
for {
var err error
ctx, cancel := context.WithTimeout(c.context(cli), RequestTimeout)
resp, err = cli.Get(ctx, makeKeyPrefix(key), clientv3.WithPrefix())
ctx, cancel := context.WithTimeout(cli.Ctx(), RequestTimeout)
if key.exactMatch {
resp, err = cli.Get(ctx, key.key)
} else {
resp, err = cli.Get(ctx, makeKeyPrefix(key.key), clientv3.WithPrefix())
}
cancel()
if err == nil {
break
}
logx.Errorf("%s, key is %s", err.Error(), key)
time.Sleep(coolDownInterval)
logc.Errorf(cli.Ctx(), "%s, key: %s, exactMatch: %t", err.Error(), key.key, key.exactMatch)
time.Sleep(coolDownUnstable.AroundDuration(coolDownInterval))
}
var kvs []KV
@@ -247,16 +321,13 @@ func (c *cluster) load(cli EtcdClient, key string) int64 {
return resp.Header.Revision
}
func (c *cluster) monitor(key string, l UpdateListener) error {
c.lock.Lock()
c.listeners[key] = append(c.listeners[key], l)
c.lock.Unlock()
func (c *cluster) monitor(key watchKey, l UpdateListener) error {
cli, err := c.getClient()
if err != nil {
return err
}
c.addListener(key, l)
rev := c.load(cli, key)
c.watchGroup.Run(func() {
c.watch(cli, key, rev)
@@ -278,16 +349,22 @@ func (c *cluster) newClient() (EtcdClient, error) {
func (c *cluster) reload(cli EtcdClient) {
c.lock.Lock()
// cancel the previous watches
close(c.done)
c.watchGroup.Wait()
var keys []watchKey
for wk, wval := range c.watchers {
keys = append(keys, wk)
if wval.cancel != nil {
wval.cancel()
}
}
c.done = make(chan lang.PlaceholderType)
c.watchGroup = threading.NewRoutineGroup()
var keys []string
for k := range c.listeners {
keys = append(keys, k)
}
c.lock.Unlock()
// start new watches
for _, key := range keys {
k := key
c.watchGroup.Run(func() {
@@ -297,7 +374,7 @@ func (c *cluster) reload(cli EtcdClient) {
}
}
func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
func (c *cluster) watch(cli EtcdClient, key watchKey, rev int64) {
for {
err := c.watchStream(cli, key, rev)
if err == nil {
@@ -305,24 +382,17 @@ func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
}
if rev != 0 && errors.Is(err, rpctypes.ErrCompacted) {
logx.Errorf("etcd watch stream has been compacted, try to reload, rev %d", rev)
logc.Errorf(cli.Ctx(), "etcd watch stream has been compacted, try to reload, rev %d", rev)
rev = c.load(cli, key)
}
// log the error and retry
logx.Error(err)
logc.Error(cli.Ctx(), err)
}
}
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) error {
var rch clientv3.WatchChan
if rev != 0 {
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key),
clientv3.WithPrefix(), clientv3.WithRev(rev+1))
} else {
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key),
clientv3.WithPrefix())
}
func (c *cluster) watchStream(cli EtcdClient, key watchKey, rev int64) error {
ctx, rch := c.setupWatch(cli, key, rev)
for {
select {
@@ -337,13 +407,47 @@ func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) error {
return fmt.Errorf("etcd monitor chan error: %w", wresp.Err())
}
c.handleWatchEvents(key, wresp.Events)
c.handleWatchEvents(ctx, key, wresp.Events)
case <-ctx.Done():
return nil
case <-c.done:
return nil
}
}
}
func (c *cluster) setupWatch(cli EtcdClient, key watchKey, rev int64) (context.Context, clientv3.WatchChan) {
var (
rch clientv3.WatchChan
ops []clientv3.OpOption
wkey = key.key
)
if !key.exactMatch {
wkey = makeKeyPrefix(key.key)
ops = append(ops, clientv3.WithPrefix())
}
if rev != 0 {
ops = append(ops, clientv3.WithRev(rev+1))
}
ctx, cancel := context.WithCancel(cli.Ctx())
if watcher, ok := c.watchers[key]; ok {
watcher.cancel = cancel
} else {
val := newWatchValue()
val.cancel = cancel
c.lock.Lock()
c.watchers[key] = val
c.lock.Unlock()
}
rch = cli.Watch(clientv3.WithRequireLeader(ctx), wkey, ops...)
return ctx, rch
}
func (c *cluster) watchConnState(cli EtcdClient) {
watcher := newStateWatcher()
watcher.addListener(func() {
@@ -372,6 +476,28 @@ func DialClient(endpoints []string) (EtcdClient, error) {
return clientv3.New(cfg)
}
func calculateChanges(oldVals, newVals map[string]string) (add, remove []KV) {
for k, v := range newVals {
if val, ok := oldVals[k]; !ok || v != val {
add = append(add, KV{
Key: k,
Val: v,
})
}
}
for k, v := range oldVals {
if val, ok := newVals[k]; !ok || v != val {
remove = append(remove, KV{
Key: k,
Val: v,
})
}
}
return add, remove
}
func getClusterKey(endpoints []string) string {
sort.Strings(endpoints)
return strings.Join(endpoints, endpointsSeparator)
@@ -380,3 +506,10 @@ func getClusterKey(endpoints []string) string {
func makeKeyPrefix(key string) string {
return fmt.Sprintf("%s%c", key, Delimiter)
}
// NewClient returns a watchValue that make sure values are not nil.
func newWatchValue() *watchValue {
return &watchValue{
values: make(map[string]string),
}
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/core/threading"
"go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
@@ -38,9 +39,9 @@ func setMockClient(cli EtcdClient) func() {
func TestGetCluster(t *testing.T) {
AddAccount([]string{"first"}, "foo", "bar")
c1, _ := GetRegistry().getCluster([]string{"first"})
c2, _ := GetRegistry().getCluster([]string{"second"})
c3, _ := GetRegistry().getCluster([]string{"first"})
c1, _ := GetRegistry().getOrCreateCluster([]string{"first"})
c2, _ := GetRegistry().getOrCreateCluster([]string{"second"})
c3, _ := GetRegistry().getOrCreateCluster([]string{"first"})
assert.Equal(t, c1, c3)
assert.NotEqual(t, c1, c2)
}
@@ -50,6 +51,36 @@ func TestGetClusterKey(t *testing.T) {
getClusterKey([]string{"remotehost:5678", "localhost:1234"}))
}
func TestUnmonitor(t *testing.T) {
t.Run("no listener", func(t *testing.T) {
reg := &Registry{
clusters: map[string]*cluster{},
}
assert.NotPanics(t, func() {
reg.Unmonitor([]string{"any"}, "any", false, nil)
})
})
t.Run("no value", func(t *testing.T) {
reg := &Registry{
clusters: map[string]*cluster{
"any": {
watchers: map[watchKey]*watchValue{
{
key: "any",
}: {
values: map[string]string{},
},
},
},
},
}
assert.NotPanics(t, func() {
reg.Unmonitor([]string{"any"}, "another", false, nil)
})
})
}
func TestCluster_HandleChanges(t *testing.T) {
ctrl := gomock.NewController(t)
l := NewMockUpdateListener(ctrl)
@@ -78,8 +109,14 @@ func TestCluster_HandleChanges(t *testing.T) {
Val: "4",
})
c := newCluster([]string{"any"})
c.listeners["any"] = []UpdateListener{l}
c.handleChanges("any", []KV{
key := watchKey{
key: "any",
exactMatch: false,
}
c.watchers[key] = &watchValue{
listeners: []UpdateListener{l},
}
c.handleChanges(key, []KV{
{
Key: "first",
Val: "1",
@@ -92,8 +129,8 @@ func TestCluster_HandleChanges(t *testing.T) {
assert.EqualValues(t, map[string]string{
"first": "1",
"second": "2",
}, c.values["any"])
c.handleChanges("any", []KV{
}, c.watchers[key].values)
c.handleChanges(key, []KV{
{
Key: "third",
Val: "3",
@@ -106,7 +143,7 @@ func TestCluster_HandleChanges(t *testing.T) {
assert.EqualValues(t, map[string]string{
"third": "3",
"fourth": "4",
}, c.values["any"])
}, c.watchers[key].values)
}
func TestCluster_Load(t *testing.T) {
@@ -126,9 +163,11 @@ func TestCluster_Load(t *testing.T) {
}, nil)
cli.EXPECT().Ctx().Return(context.Background())
c := &cluster{
values: make(map[string]map[string]string),
watchers: make(map[watchKey]*watchValue),
}
c.load(cli, "any")
c.load(cli, watchKey{
key: "any",
})
}
func TestCluster_Watch(t *testing.T) {
@@ -160,11 +199,16 @@ func TestCluster_Watch(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
c := &cluster{
listeners: make(map[string][]UpdateListener),
values: make(map[string]map[string]string),
watchers: make(map[watchKey]*watchValue),
}
key := watchKey{
key: "any",
}
listener := NewMockUpdateListener(ctrl)
c.listeners["any"] = []UpdateListener{listener}
c.watchers[key] = &watchValue{
listeners: []UpdateListener{listener},
values: make(map[string]string),
}
listener.EXPECT().OnAdd(gomock.Any()).Do(func(kv KV) {
assert.Equal(t, "hello", kv.Key)
assert.Equal(t, "world", kv.Val)
@@ -173,7 +217,7 @@ func TestCluster_Watch(t *testing.T) {
listener.EXPECT().OnDelete(gomock.Any()).Do(func(_ any) {
wg.Done()
}).MaxTimes(1)
go c.watch(cli, "any", 0)
go c.watch(cli, key, 0)
ch <- clientv3.WatchResponse{
Events: []*clientv3.Event{
{
@@ -211,17 +255,111 @@ func TestClusterWatch_RespFailures(t *testing.T) {
ch := make(chan clientv3.WatchResponse)
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
c := new(cluster)
c := &cluster{
watchers: make(map[watchKey]*watchValue),
}
c.done = make(chan lang.PlaceholderType)
go func() {
ch <- resp
close(c.done)
}()
c.watch(cli, "any", 0)
key := watchKey{
key: "any",
}
c.watch(cli, key, 0)
})
}
}
func TestCluster_getCurrent(t *testing.T) {
t.Run("no value", func(t *testing.T) {
c := &cluster{
watchers: map[watchKey]*watchValue{
{
key: "any",
}: {
values: map[string]string{},
},
},
}
assert.Nil(t, c.getCurrent(watchKey{
key: "another",
}))
})
}
func TestCluster_handleWatchEvents(t *testing.T) {
t.Run("no value", func(t *testing.T) {
c := &cluster{
watchers: map[watchKey]*watchValue{
{
key: "any",
}: {
values: map[string]string{},
},
},
}
assert.NotPanics(t, func() {
c.handleWatchEvents(context.Background(), watchKey{
key: "another",
}, nil)
})
})
}
func TestCluster_addListener(t *testing.T) {
t.Run("has listener", func(t *testing.T) {
c := &cluster{
watchers: map[watchKey]*watchValue{
{
key: "any",
}: {
listeners: make([]UpdateListener, 0),
},
},
}
assert.NotPanics(t, func() {
c.addListener(watchKey{
key: "any",
}, nil)
})
})
t.Run("no listener", func(t *testing.T) {
c := &cluster{
watchers: map[watchKey]*watchValue{
{
key: "any",
}: {
listeners: make([]UpdateListener, 0),
},
},
}
assert.NotPanics(t, func() {
c.addListener(watchKey{
key: "another",
}, nil)
})
})
}
func TestCluster_reload(t *testing.T) {
c := &cluster{
watchers: map[watchKey]*watchValue{},
watchGroup: threading.NewRoutineGroup(),
done: make(chan lang.PlaceholderType),
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := NewMockEtcdClient(ctrl)
restore := setMockClient(cli)
defer restore()
assert.NotPanics(t, func() {
c.reload(cli)
})
}
func TestClusterWatch_CloseChan(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -231,13 +369,17 @@ func TestClusterWatch_CloseChan(t *testing.T) {
ch := make(chan clientv3.WatchResponse)
cli.EXPECT().Watch(gomock.Any(), "any/", gomock.Any()).Return(ch).AnyTimes()
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
c := new(cluster)
c := &cluster{
watchers: make(map[watchKey]*watchValue),
}
c.done = make(chan lang.PlaceholderType)
go func() {
close(ch)
close(c.done)
}()
c.watch(cli, "any", 0)
c.watch(cli, watchKey{
key: "any",
}, 0)
}
func TestValueOnlyContext(t *testing.T) {
@@ -280,16 +422,59 @@ func TestRegistry_Monitor(t *testing.T) {
GetRegistry().lock.Lock()
GetRegistry().clusters = map[string]*cluster{
getClusterKey(endpoints): {
listeners: map[string][]UpdateListener{},
values: map[string]map[string]string{
"foo": {
"bar": "baz",
watchers: map[watchKey]*watchValue{
watchKey{
key: "foo",
exactMatch: true,
}: {
values: map[string]string{
"bar": "baz",
},
},
},
},
}
GetRegistry().lock.Unlock()
assert.Error(t, GetRegistry().Monitor(endpoints, "foo", new(mockListener)))
assert.Error(t, GetRegistry().Monitor(endpoints, "foo", false, new(mockListener)))
}
func TestRegistry_Unmonitor(t *testing.T) {
svr, err := mockserver.StartMockServers(1)
assert.NoError(t, err)
svr.StartAt(0)
_, cancel := context.WithCancel(context.Background())
endpoints := []string{svr.Servers[0].Address}
GetRegistry().lock.Lock()
GetRegistry().clusters = map[string]*cluster{
getClusterKey(endpoints): {
watchers: map[watchKey]*watchValue{
watchKey{
key: "foo",
exactMatch: true,
}: {
values: map[string]string{
"bar": "baz",
},
cancel: cancel,
},
},
},
}
GetRegistry().lock.Unlock()
l := new(mockListener)
assert.NoError(t, GetRegistry().Monitor(endpoints, "foo", true, l))
watchVals := GetRegistry().clusters[getClusterKey(endpoints)].watchers[watchKey{
key: "foo",
exactMatch: true,
}]
assert.Equal(t, 1, len(watchVals.listeners))
GetRegistry().Unmonitor(endpoints, "foo", true, l)
watchVals = GetRegistry().clusters[getClusterKey(endpoints)].watchers[watchKey{
key: "foo",
exactMatch: true,
}]
assert.Nil(t, watchVals)
}
type mockListener struct {

View File

@@ -10,6 +10,7 @@ type (
}
// UpdateListener wraps the OnAdd and OnDelete methods.
// The implementation should be thread-safe and idempotent.
UpdateListener interface {
OnAdd(kv KV)
OnDelete(kv KV)

View File

@@ -5,6 +5,7 @@ import (
"github.com/zeromicro/go-zero/core/discov/internal"
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/logc"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/syncx"
@@ -91,12 +92,12 @@ func (p *Publisher) doKeepAlive() error {
default:
cli, err := p.doRegister()
if err != nil {
logx.Errorf("etcd publisher doRegister: %s", err.Error())
logc.Errorf(cli.Ctx(), "etcd publisher doRegister: %s", err.Error())
break
}
if err := p.keepAliveAsync(cli); err != nil {
logx.Errorf("etcd publisher keepAliveAsync: %s", err.Error())
logc.Errorf(cli.Ctx(), "etcd publisher keepAliveAsync: %s", err.Error())
break
}
@@ -130,17 +131,17 @@ func (p *Publisher) keepAliveAsync(cli internal.EtcdClient) error {
if !ok {
p.revoke(cli)
if err := p.doKeepAlive(); err != nil {
logx.Errorf("etcd publisher KeepAlive: %s", err.Error())
logc.Errorf(cli.Ctx(), "etcd publisher KeepAlive: %s", err.Error())
}
return
}
case <-p.pauseChan:
logx.Infof("paused etcd renew, key: %s, value: %s", p.key, p.value)
logc.Infof(cli.Ctx(), "paused etcd renew, key: %s, value: %s", p.key, p.value)
p.revoke(cli)
select {
case <-p.resumeChan:
if err := p.doKeepAlive(); err != nil {
logx.Errorf("etcd publisher KeepAlive: %s", err.Error())
logc.Errorf(cli.Ctx(), "etcd publisher KeepAlive: %s", err.Error())
}
return
case <-p.quit.Done():
@@ -175,7 +176,7 @@ func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, erro
func (p *Publisher) revoke(cli internal.EtcdClient) {
if _, err := cli.Revoke(cli.Ctx(), p.lease); err != nil {
logx.Errorf("etcd publisher revoke: %s", err.Error())
logc.Errorf(cli.Ctx(), "etcd publisher revoke: %s", err.Error())
}
}

View File

@@ -15,9 +15,11 @@ type (
// A Subscriber is used to subscribe the given key on an etcd cluster.
Subscriber struct {
endpoints []string
exclusive bool
items *container
endpoints []string
exclusive bool
key string
exactMatch bool
items *container
}
)
@@ -28,13 +30,14 @@ type (
func NewSubscriber(endpoints []string, key string, opts ...SubOption) (*Subscriber, error) {
sub := &Subscriber{
endpoints: endpoints,
key: key,
}
for _, opt := range opts {
opt(sub)
}
sub.items = newContainer(sub.exclusive)
if err := internal.GetRegistry().Monitor(endpoints, key, sub.items); err != nil {
if err := internal.GetRegistry().Monitor(endpoints, key, sub.exactMatch, sub.items); err != nil {
return nil, err
}
@@ -46,6 +49,11 @@ func (s *Subscriber) AddListener(listener func()) {
s.items.addListener(listener)
}
// Close closes the subscriber.
func (s *Subscriber) Close() {
internal.GetRegistry().Unmonitor(s.endpoints, s.key, s.exactMatch, s.items)
}
// Values returns all the subscription values.
func (s *Subscriber) Values() []string {
return s.items.getValues()
@@ -59,6 +67,13 @@ func Exclusive() SubOption {
}
}
// WithExactMatch turn off querying using key prefixes.
func WithExactMatch() SubOption {
return func(sub *Subscriber) {
sub.exactMatch = true
}
}
// WithSubEtcdAccount provides the etcd username/password.
func WithSubEtcdAccount(user, pass string) SubOption {
return func(sub *Subscriber) {

View File

@@ -225,3 +225,28 @@ func TestWithSubEtcdAccount(t *testing.T) {
assert.Equal(t, user, account.User)
assert.Equal(t, "bar", account.Pass)
}
func TestWithExactMatch(t *testing.T) {
sub := new(Subscriber)
WithExactMatch()(sub)
sub.items = newContainer(sub.exclusive)
var count int32
sub.AddListener(func() {
atomic.AddInt32(&count, 1)
})
sub.items.notifyChange()
assert.Empty(t, sub.Values())
assert.Equal(t, int32(1), atomic.LoadInt32(&count))
}
func TestSubscriberClose(t *testing.T) {
l := newContainer(false)
sub := &Subscriber{
endpoints: []string{"localhost:12379"},
key: "foo",
items: l,
}
assert.NotPanics(t, func() {
sub.Close()
})
}

View File

@@ -1,21 +1,17 @@
package errorx
import (
"bytes"
"errors"
"sync"
)
type (
// A BatchError is an error that can hold multiple errors.
BatchError struct {
errs errorArray
lock sync.Mutex
}
// BatchError is an error that can hold multiple errors.
type BatchError struct {
errs []error
lock sync.RWMutex
}
errorArray []error
)
// Add adds errs to be, nil errors are ignored.
// Add adds one or more non-nil errors to the BatchError instance.
func (be *BatchError) Add(errs ...error) {
be.lock.Lock()
defer be.lock.Unlock()
@@ -27,39 +23,20 @@ func (be *BatchError) Add(errs ...error) {
}
}
// Err returns an error that represents all errors.
// Err returns an error that represents all accumulated errors.
// It returns nil if there are no errors.
func (be *BatchError) Err() error {
be.lock.Lock()
defer be.lock.Unlock()
be.lock.RLock()
defer be.lock.RUnlock()
switch len(be.errs) {
case 0:
return nil
case 1:
return be.errs[0]
default:
return be.errs
}
// If there are no non-nil errors, errors.Join(...) returns nil.
return errors.Join(be.errs...)
}
// NotNil checks if any error inside.
// NotNil checks if there is at least one error inside the BatchError.
func (be *BatchError) NotNil() bool {
be.lock.Lock()
defer be.lock.Unlock()
be.lock.RLock()
defer be.lock.RUnlock()
return len(be.errs) > 0
}
// Error returns a string that represents inside errors.
func (ea errorArray) Error() string {
var buf bytes.Buffer
for i := range ea {
if i > 0 {
buf.WriteByte('\n')
}
buf.WriteString(ea[i].Error())
}
return buf.String()
}

View File

@@ -66,3 +66,82 @@ func TestBatchErrorConcurrentAdd(t *testing.T) {
assert.Equal(t, count, len(batch.errs))
assert.True(t, batch.NotNil())
}
func TestBatchError_Unwrap(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var be BatchError
assert.Nil(t, be.Err())
assert.True(t, errors.Is(be.Err(), nil))
})
t.Run("one error", func(t *testing.T) {
var errFoo = errors.New("foo")
var errBar = errors.New("bar")
var be BatchError
be.Add(errFoo)
assert.True(t, errors.Is(be.Err(), errFoo))
assert.False(t, errors.Is(be.Err(), errBar))
})
t.Run("two errors", func(t *testing.T) {
var errFoo = errors.New("foo")
var errBar = errors.New("bar")
var errBaz = errors.New("baz")
var be BatchError
be.Add(errFoo)
be.Add(errBar)
assert.True(t, errors.Is(be.Err(), errFoo))
assert.True(t, errors.Is(be.Err(), errBar))
assert.False(t, errors.Is(be.Err(), errBaz))
})
}
func TestBatchError_Add(t *testing.T) {
var be BatchError
// Test adding nil errors
be.Add(nil, nil)
assert.False(t, be.NotNil(), "Expected BatchError to be empty after adding nil errors")
// Test adding non-nil errors
err1 := errors.New("error 1")
err2 := errors.New("error 2")
be.Add(err1, err2)
assert.True(t, be.NotNil(), "Expected BatchError to be non-empty after adding errors")
// Test adding a mix of nil and non-nil errors
err3 := errors.New("error 3")
be.Add(nil, err3, nil)
assert.True(t, be.NotNil(), "Expected BatchError to be non-empty after adding a mix of nil and non-nil errors")
}
func TestBatchError_Err(t *testing.T) {
var be BatchError
// Test Err() on empty BatchError
assert.Nil(t, be.Err(), "Expected nil error for empty BatchError")
// Test Err() with multiple errors
err1 := errors.New("error 1")
err2 := errors.New("error 2")
be.Add(err1, err2)
combinedErr := be.Err()
assert.NotNil(t, combinedErr, "Expected nil error for BatchError with multiple errors")
// Check if the combined error contains both error messages
errString := combinedErr.Error()
assert.Truef(t, errors.Is(combinedErr, err1), "Combined error doesn't contain first error: %s", errString)
assert.Truef(t, errors.Is(combinedErr, err2), "Combined error doesn't contain second error: %s", errString)
}
func TestBatchError_NotNil(t *testing.T) {
var be BatchError
// Test NotNil() on empty BatchError
assert.Nil(t, be.Err(), "Expected nil error for empty BatchError")
// Test NotNil() after adding an error
be.Add(errors.New("test error"))
assert.NotNil(t, be.Err(), "Expected non-nil error after adding an error")
}

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package fs

View File

@@ -86,21 +86,16 @@ func TestConsistentHashIncrementalTransfer(t *testing.T) {
func TestConsistentHashTransferOnFailure(t *testing.T) {
index := 41
keys, newKeys := getKeysBeforeAndAfterFailure(t, "localhost:", index)
var transferred int
for k, v := range newKeys {
if v != keys[k] {
transferred++
}
}
ratio := float32(transferred) / float32(requestSize)
assert.True(t, ratio < 2.5/float32(keySize), fmt.Sprintf("%d: %f", index, ratio))
ratioNotExists := getTransferRatioOnFailure(t, index)
assert.True(t, ratioNotExists == 0, fmt.Sprintf("%d: %f", index, ratioNotExists))
index = 13
ratio := getTransferRatioOnFailure(t, index)
assert.True(t, ratio < 2.5/keySize, fmt.Sprintf("%d: %f", index, ratio))
}
func TestConsistentHashLeastTransferOnFailure(t *testing.T) {
prefix := "localhost:"
index := 41
index := 13
keys, newKeys := getKeysBeforeAndAfterFailure(t, prefix, index)
for k, v := range keys {
newV := newKeys[k]
@@ -164,6 +159,17 @@ func getKeysBeforeAndAfterFailure(t *testing.T, prefix string, index int) (map[i
return keys, newKeys
}
func getTransferRatioOnFailure(t *testing.T, index int) float32 {
keys, newKeys := getKeysBeforeAndAfterFailure(t, "localhost:", index)
var transferred int
for k, v := range newKeys {
if v != keys[k] {
transferred++
}
}
return float32(transferred) / float32(requestSize)
}
type mockNode struct {
addr string
id int

View File

@@ -2,7 +2,7 @@ package hash
import (
"crypto/md5"
"fmt"
"encoding/hex"
"github.com/spaolacci/murmur3"
)
@@ -20,6 +20,7 @@ func Md5(data []byte) []byte {
}
// Md5Hex returns the md5 hex string of data.
// This function is optimized for better performance than fmt.Sprintf.
func Md5Hex(data []byte) string {
return fmt.Sprintf("%x", Md5(data))
return hex.EncodeToString(Md5(data))
}

View File

@@ -37,6 +37,13 @@ func Debugf(ctx context.Context, format string, v ...interface{}) {
getLogger(ctx).Debugf(format, v...)
}
// Debugfn writes fn result into access log.
// This is useful when the function is expensive to compute,
// and we want to log it only when necessary.
func Debugfn(ctx context.Context, fn func() any) {
getLogger(ctx).Debugfn(fn)
}
// Debugv writes v into access log with json content.
func Debugv(ctx context.Context, v interface{}) {
getLogger(ctx).Debugv(v)
@@ -57,6 +64,13 @@ func Errorf(ctx context.Context, format string, v ...any) {
getLogger(ctx).Errorf(fmt.Errorf(format, v...).Error())
}
// Errorfn writes fn result into error log.
// This is useful when the function is expensive to compute,
// and we want to log it only when necessary.
func Errorfn(ctx context.Context, fn func() any) {
getLogger(ctx).Errorfn(fn)
}
// Errorv writes v into error log with json content.
// No call stack attached, because not elegant to pack the messages.
func Errorv(ctx context.Context, v any) {
@@ -83,6 +97,13 @@ func Infof(ctx context.Context, format string, v ...any) {
getLogger(ctx).Infof(format, v...)
}
// Infofn writes fn result into access log.
// This is useful when the function is expensive to compute,
// and we want to log it only when necessary.
func Infofn(ctx context.Context, fn func() any) {
getLogger(ctx).Infofn(fn)
}
// Infov writes v into access log with json content.
func Infov(ctx context.Context, v any) {
getLogger(ctx).Infov(v)
@@ -127,6 +148,13 @@ func Slowf(ctx context.Context, format string, v ...any) {
getLogger(ctx).Slowf(format, v...)
}
// Slowfn writes fn result into slow log.
// This is useful when the function is expensive to compute,
// and we want to log it only when necessary.
func Slowfn(ctx context.Context, fn func() any) {
getLogger(ctx).Slowfn(fn)
}
// Slowv writes v into slow log with json content.
func Slowv(ctx context.Context, v any) {
getLogger(ctx).Slowv(v)

View File

@@ -49,6 +49,15 @@ func TestErrorf(t *testing.T) {
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorfn(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
Errorfn(context.Background(), func() any {
return fmt.Sprintf("foo %s", "bar")
})
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorv(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
@@ -77,6 +86,15 @@ func TestInfof(t *testing.T) {
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfofn(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
Infofn(context.Background(), func() any {
return fmt.Sprintf("foo %s", "bar")
})
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfov(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
@@ -105,6 +123,15 @@ func TestDebugf(t *testing.T) {
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestDebugfn(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
Debugfn(context.Background(), func() any {
return fmt.Sprintf("foo %s", "bar")
})
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestDebugv(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
@@ -148,6 +175,15 @@ func TestSlowf(t *testing.T) {
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSlowfn(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()
Slowfn(context.Background(), func() any {
return fmt.Sprintf("foo %s", "bar")
})
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSlowv(t *testing.T) {
buf := logtest.NewCollector(t)
file, line := getFileLine()

View File

@@ -42,4 +42,6 @@ type LogConf struct {
// daily: daily rotation.
// size: size limited rotation.
Rotation string `json:",default=daily,options=[daily,size]"`
// FileTimeFormat represents the time format for file name, default is `2006-01-02T15:04:05.000Z07:00`.
FileTimeFormat string `json:",optional"`
}

View File

@@ -11,6 +11,8 @@ type Logger interface {
Debug(...any)
// Debugf logs a message at debug level.
Debugf(string, ...any)
// Debugfn logs a message at debug level.
Debugfn(func() any)
// Debugv logs a message at debug level.
Debugv(any)
// Debugw logs a message at debug level.
@@ -19,6 +21,8 @@ type Logger interface {
Error(...any)
// Errorf logs a message at error level.
Errorf(string, ...any)
// Errorfn logs a message at error level.
Errorfn(func() any)
// Errorv logs a message at error level.
Errorv(any)
// Errorw logs a message at error level.
@@ -27,6 +31,8 @@ type Logger interface {
Info(...any)
// Infof logs a message at info level.
Infof(string, ...any)
// Infofn logs a message at info level.
Infofn(func() any)
// Infov logs a message at info level.
Infov(any)
// Infow logs a message at info level.
@@ -35,6 +41,8 @@ type Logger interface {
Slow(...any)
// Slowf logs a message at slow level.
Slowf(string, ...any)
// Slowfn logs a message at slow level.
Slowfn(func() any)
// Slowv logs a message at slow level.
Slowv(any)
// Sloww logs a message at slow level.

View File

@@ -100,6 +100,14 @@ func Debugf(format string, v ...any) {
}
}
// Debugfn writes function result into access log if debug level enabled.
// This is useful when the function is expensive to call and debug level disabled.
func Debugfn(fn func() any) {
if shallLog(DebugLevel) {
writeDebug(fn())
}
}
// Debugv writes v into access log with json content.
func Debugv(v any) {
if shallLog(DebugLevel) {
@@ -139,6 +147,13 @@ func Errorf(format string, v ...any) {
}
}
// Errorfn writes function result into error log.
func Errorfn(fn func() any) {
if shallLog(ErrorLevel) {
writeError(fn())
}
}
// ErrorStack writes v along with call stack into error log.
func ErrorStack(v ...any) {
if shallLog(ErrorLevel) {
@@ -222,6 +237,14 @@ func Infof(format string, v ...any) {
}
}
// Infofn writes function result into access log.
// This is useful when the function is expensive to call and info level disabled.
func Infofn(fn func() any) {
if shallLog(InfoLevel) {
writeInfo(fn())
}
}
// Infov writes v into access log with json content.
func Infov(v any) {
if shallLog(InfoLevel) {
@@ -294,6 +317,10 @@ func SetUp(c LogConf) (err error) {
timeFormat = c.TimeFormat
}
if len(c.FileTimeFormat) > 0 {
fileTimeFormat = c.FileTimeFormat
}
atomic.StoreUint32(&maxContentLength, c.MaxContentLength)
switch c.Encoding {
@@ -344,6 +371,14 @@ func Slowf(format string, v ...any) {
}
}
// Slowfn writes function result into slow log.
// This is useful when the function is expensive to call and slow level disabled.
func Slowfn(fn func() any) {
if shallLog(ErrorLevel) {
writeSlow(fn())
}
}
// Slowv writes v into slow log with json content.
func Slowv(v any) {
if shallLog(ErrorLevel) {
@@ -525,7 +560,7 @@ func shallLogStat() bool {
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeDebug(val any, fields ...LogField) {
getWriter().Debug(val, addCaller(fields...)...)
getWriter().Debug(val, mergeGlobalFields(addCaller(fields...))...)
}
// writeError writes v into the error log.
@@ -533,7 +568,7 @@ func writeDebug(val any, fields ...LogField) {
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeError(val any, fields ...LogField) {
getWriter().Error(val, addCaller(fields...)...)
getWriter().Error(val, mergeGlobalFields(addCaller(fields...))...)
}
// writeInfo writes v into info log.
@@ -541,7 +576,7 @@ func writeError(val any, fields ...LogField) {
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeInfo(val any, fields ...LogField) {
getWriter().Info(val, addCaller(fields...)...)
getWriter().Info(val, mergeGlobalFields(addCaller(fields...))...)
}
// writeSevere writes v into severe log.
@@ -557,7 +592,7 @@ func writeSevere(msg string) {
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeSlow(val any, fields ...LogField) {
getWriter().Slow(val, addCaller(fields...)...)
getWriter().Slow(val, mergeGlobalFields(addCaller(fields...))...)
}
// writeStack writes v into stack log.
@@ -573,5 +608,5 @@ func writeStack(msg string) {
// If we check shallLog here, the fmt.Sprint might be called even if the log level is not enabled.
// The caller should check shallLog before calling this function.
func writeStat(msg string) {
getWriter().Stat(msg, addCaller()...)
getWriter().Stat(msg, mergeGlobalFields(addCaller())...)
}

View File

@@ -248,6 +248,32 @@ func TestStructedLogDebugf(t *testing.T) {
})
}
func TestStructedLogDebugfn(t *testing.T) {
t.Run("debugfn with output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelDebug, w, func(v ...any) {
Debugfn(func() any {
return fmt.Sprint(v...)
})
})
})
t.Run("debugfn without output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogEmpty(t, w, InfoLevel, func(v ...any) {
Debugfn(func() any {
return fmt.Sprint(v...)
})
})
})
}
func TestStructedLogDebugv(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
@@ -288,6 +314,32 @@ func TestStructedLogErrorf(t *testing.T) {
})
}
func TestStructedLogErrorfn(t *testing.T) {
t.Run("errorfn with output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...any) {
Errorfn(func() any {
return fmt.Sprint(v...)
})
})
})
t.Run("errorfn without output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogEmpty(t, w, SevereLevel, func(v ...any) {
Errorfn(func() any {
return fmt.Sprint(v...)
})
})
})
}
func TestStructedLogErrorv(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
@@ -328,6 +380,32 @@ func TestStructedLogInfof(t *testing.T) {
})
}
func TestStructedInfofn(t *testing.T) {
t.Run("infofn with output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...any) {
Infofn(func() any {
return fmt.Sprint(v...)
})
})
})
t.Run("infofn without output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogEmpty(t, w, ErrorLevel, func(v ...any) {
Infofn(func() any {
return fmt.Sprint(v...)
})
})
})
}
func TestStructedLogInfov(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
@@ -451,6 +529,17 @@ func TestStructedLogInfoConsoleText(t *testing.T) {
})
}
func TestInfofnWithErrorLevel(t *testing.T) {
called := false
SetLevel(ErrorLevel)
defer SetLevel(DebugLevel)
Infofn(func() any {
called = true
return "info log"
})
assert.False(t, called)
}
func TestStructedLogSlow(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
@@ -471,6 +560,32 @@ func TestStructedLogSlowf(t *testing.T) {
})
}
func TestStructedLogSlowfn(t *testing.T) {
t.Run("slowfn with output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...any) {
Slowfn(func() any {
return fmt.Sprint(v...)
})
})
})
t.Run("slowfn without output", func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogEmpty(t, w, SevereLevel, func(v ...any) {
Slowfn(func() any {
return fmt.Sprint(v...)
})
})
})
}
func TestStructedLogSlowv(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
@@ -847,15 +962,26 @@ func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...any)) {
assert.True(t, strings.Contains(w.String(), message))
}
func doTestStructedLogEmpty(t *testing.T, w *mockWriter, level uint32, write func(...any)) {
olevel := atomic.LoadUint32(&logLevel)
SetLevel(level)
defer SetLevel(olevel)
const message = "hello there"
write(message)
assert.Empty(t, w.String())
}
func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) {
writer.Store(nil)
SetUp(LogConf{
Mode: mode,
Level: "debug",
Path: "/dev/null",
Encoding: plainEncoding,
Stat: false,
TimeFormat: time.RFC3339,
Mode: mode,
Level: "debug",
Path: "/dev/null",
Encoding: plainEncoding,
Stat: false,
TimeFormat: time.RFC3339,
FileTimeFormat: time.DateTime,
})
SetUp(LogConf{
Mode: mode,

View File

@@ -52,6 +52,12 @@ func (l *richLogger) Debugf(format string, v ...any) {
}
}
func (l *richLogger) Debugfn(fn func() any) {
if shallLog(DebugLevel) {
l.debug(fn())
}
}
func (l *richLogger) Debugv(v any) {
if shallLog(DebugLevel) {
l.debug(v)
@@ -76,6 +82,12 @@ func (l *richLogger) Errorf(format string, v ...any) {
}
}
func (l *richLogger) Errorfn(fn func() any) {
if shallLog(ErrorLevel) {
l.err(fn())
}
}
func (l *richLogger) Errorv(v any) {
if shallLog(ErrorLevel) {
l.err(v)
@@ -100,6 +112,12 @@ func (l *richLogger) Infof(format string, v ...any) {
}
}
func (l *richLogger) Infofn(fn func() any) {
if shallLog(InfoLevel) {
l.info(fn())
}
}
func (l *richLogger) Infov(v any) {
if shallLog(InfoLevel) {
l.info(v)
@@ -124,6 +142,12 @@ func (l *richLogger) Slowf(format string, v ...any) {
}
}
func (l *richLogger) Slowfn(fn func() any) {
if shallLog(ErrorLevel) {
l.slow(fn())
}
}
func (l *richLogger) Slowv(v any) {
if shallLog(ErrorLevel) {
l.slow(v)
@@ -182,7 +206,9 @@ func (l *richLogger) WithFields(fields ...LogField) Logger {
func (l *richLogger) buildFields(fields ...LogField) []LogField {
fields = append(l.fields, fields...)
// caller field should always appear together with global fields
fields = append(fields, Field(callerKey, getCaller(callerDepth+l.callerSkip)))
fields = mergeGlobalFields(fields)
if l.ctx == nil {
return fields

View File

@@ -63,6 +63,11 @@ func TestTraceDebug(t *testing.T) {
l.WithDuration(time.Second).Debugf(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Debugfn(func() any {
return testlog
})
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Debugv(testlog)
validate(t, w.String(), true, true)
w.Reset()
@@ -103,6 +108,11 @@ func TestTraceError(t *testing.T) {
l.WithDuration(time.Second).Errorf(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Errorfn(func() any {
return testlog
})
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Errorv(testlog)
validate(t, w.String(), true, true)
w.Reset()
@@ -140,6 +150,11 @@ func TestTraceInfo(t *testing.T) {
l.WithDuration(time.Second).Infof(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infofn(func() any {
return testlog
})
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infov(testlog)
validate(t, w.String(), true, true)
w.Reset()
@@ -213,6 +228,11 @@ func TestTraceSlow(t *testing.T) {
l.WithDuration(time.Second).Slowf(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Slowfn(func() any {
return testlog
})
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Slowv(testlog)
validate(t, w.String(), true, true)
w.Reset()

View File

@@ -18,8 +18,6 @@ import (
)
const (
dateFormat = "2006-01-02"
fileTimeFormat = time.RFC3339
hoursPerDay = 24
bufferSize = 100
defaultDirMode = 0o755
@@ -28,8 +26,12 @@ const (
megaBytes = 1 << 20
)
// ErrLogFileClosed is an error that indicates the log file is already closed.
var ErrLogFileClosed = errors.New("error: log file closed")
var (
// ErrLogFileClosed is an error that indicates the log file is already closed.
ErrLogFileClosed = errors.New("error: log file closed")
fileTimeFormat = time.RFC3339
)
type (
// A RotateRule interface is used to define the log rotating rules.
@@ -113,7 +115,7 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
}
var buf strings.Builder
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat)
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(time.DateOnly)
buf.WriteString(r.filename)
buf.WriteString(r.delimiter)
buf.WriteString(boundary)
@@ -422,7 +424,7 @@ func compressLogFile(file string) {
}
func getNowDate() string {
return time.Now().Format(dateFormat)
return time.Now().Format(time.DateOnly)
}
func getNowDateInRFC3339Format() string {

View File

@@ -52,7 +52,7 @@ func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
})
t.Run("temp files", func(t *testing.T) {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
_ = f1.Close()
@@ -73,7 +73,7 @@ func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
func TestDailyRotateRuleShallRotate(t *testing.T) {
var rule DailyRotateRule
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(dateFormat)
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(time.DateOnly)
assert.True(t, rule.ShallRotate(0))
}
@@ -117,12 +117,12 @@ func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) {
})
t.Run("temp files", func(t *testing.T) {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
assert.NoError(t, err)
t.Cleanup(func() {
@@ -144,12 +144,12 @@ func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) {
})
t.Run("no backups", func(t *testing.T) {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
f1, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
f2, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary)
assert.NoError(t, err)
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(dateFormat)
boundary1 := time.Now().Add(time.Hour * time.Duration(hoursPerDay) * 2).Format(time.DateOnly)
f3, err := os.CreateTemp(os.TempDir(), "go-zero-test-"+boundary1)
assert.NoError(t, err)
t.Cleanup(func() {
@@ -319,7 +319,7 @@ func TestRotateLoggerWrite(t *testing.T) {
}
// the following write calls cannot be changed to Write, because of DATA RACE.
logger.write([]byte(`foo`))
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat)
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(time.DateOnly)
logger.write([]byte(`bar`))
logger.Close()
logger.write([]byte(`baz`))
@@ -447,7 +447,7 @@ func TestRotateLoggerWithSizeLimitRotateRuleWrite(t *testing.T) {
}
// the following write calls cannot be changed to Write, because of DATA RACE.
logger.write([]byte(`foo`))
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat)
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(time.DateOnly)
logger.write([]byte(`bar`))
logger.Close()
logger.write([]byte(`baz`))

View File

@@ -17,15 +17,27 @@ import (
)
type (
// Writer is the interface for writing logs.
// It's designed to let users customize their own log writer,
// such as writing logs to a kafka, a database, or using third-party loggers.
Writer interface {
// Alert sends an alert message, if your writer implemented alerting functionality.
Alert(v any)
// Close closes the writer.
Close() error
// Debug logs a message at debug level.
Debug(v any, fields ...LogField)
// Error logs a message at error level.
Error(v any, fields ...LogField)
// Info logs a message at info level.
Info(v any, fields ...LogField)
// Severe logs a message at severe level.
Severe(v any)
// Slow logs a message at slow level.
Slow(v any, fields ...LogField)
// Stack logs a message at error level.
Stack(v any)
// Stat logs a message at stat level.
Stat(v any, fields ...LogField)
}
@@ -324,20 +336,6 @@ func buildPlainFields(fields logEntry) []string {
return items
}
func combineGlobalFields(fields []LogField) []LogField {
globals := globalFields.Load()
if globals == nil {
return fields
}
gf := globals.([]LogField)
ret := make([]LogField, 0, len(gf)+len(fields))
ret = append(ret, gf...)
ret = append(ret, fields...)
return ret
}
func marshalJson(t interface{}) ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
@@ -352,6 +350,20 @@ func marshalJson(t interface{}) ([]byte, error) {
return buf.Bytes(), err
}
func mergeGlobalFields(fields []LogField) []LogField {
globals := globalFields.Load()
if globals == nil {
return fields
}
gf := globals.([]LogField)
ret := make([]LogField, 0, len(gf)+len(fields))
ret = append(ret, gf...)
ret = append(ret, fields...)
return ret
}
func output(writer io.Writer, level string, val any, fields ...LogField) {
// only truncate string content, don't know how to truncate the values of other types.
if v, ok := val.(string); ok {
@@ -362,7 +374,6 @@ func output(writer io.Writer, level string, val any, fields ...LogField) {
}
}
fields = combineGlobalFields(fields)
// +3 for timestamp, level and content
entry := make(logEntry, len(fields)+3)
for _, field := range fields {

View File

@@ -13,6 +13,15 @@ const (
// Marshal marshals the given val and returns the map that contains the fields.
// optional=another is not implemented, and it's hard to implement and not commonly used.
// support anonymous field, e.g.:
//
// type Foo struct {
// Token string `header:"token"`
// }
// type FooB struct {
// Foo
// Bar string `json:"bar"`
// }
func Marshal(val any) (map[string]map[string]any, error) {
ret := make(map[string]map[string]any)
tp := reflect.TypeOf(val)
@@ -44,6 +53,16 @@ func getTag(field reflect.StructField) (string, bool) {
return strings.TrimSpace(tag), false
}
func insertValue(collector map[string]map[string]any, tag string, key string, val any) {
if m, ok := collector[tag]; ok {
m[key] = val
} else {
collector[tag] = map[string]any{
key: val,
}
}
}
func processMember(field reflect.StructField, value reflect.Value,
collector map[string]map[string]any) error {
var key string
@@ -69,15 +88,20 @@ func processMember(field reflect.StructField, value reflect.Value,
val = fmt.Sprint(val)
}
m, ok := collector[tag]
if ok {
m[key] = val
} else {
m = map[string]any{
key: val,
if field.Anonymous {
anonCollector, err := Marshal(val)
if err != nil {
return err
}
for anonTag, anonMap := range anonCollector {
for anonKey, anonVal := range anonMap {
insertValue(collector, anonTag, anonKey, anonVal)
}
}
} else {
insertValue(collector, tag, key, val)
}
collector[tag] = m
return nil
}
@@ -118,7 +142,7 @@ func validateOptional(field reflect.StructField, value reflect.Value) error {
if value.IsNil() {
return fmt.Errorf("field %q is nil", field.Name)
}
case reflect.Array, reflect.Slice, reflect.Map:
case reflect.Slice, reflect.Map:
if value.IsNil() || value.Len() == 0 {
return fmt.Errorf("field %q is empty", field.Name)
}

View File

@@ -27,6 +27,124 @@ func TestMarshal(t *testing.T) {
assert.True(t, m[emptyTag]["Anonymous"].(bool))
}
func TestMarshal_Anonymous(t *testing.T) {
t.Run("anonymous", func(t *testing.T) {
type BaseHeader struct {
Token string `header:"token"`
}
v := struct {
Name string `json:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
BaseHeader
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
BaseHeader: BaseHeader{
Token: "token_xxx",
},
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "kevin", m["json"]["name"])
assert.Equal(t, "shanghai", m["json"]["address"])
assert.Equal(t, 20, m["json"]["age"].(int))
assert.Equal(t, "token_xxx", m["header"]["token"])
v1 := struct {
Name string `json:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
BaseHeader
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
}
m1, err1 := Marshal(v1)
assert.Nil(t, err1)
assert.Equal(t, "kevin", m1["json"]["name"])
assert.Equal(t, "shanghai", m1["json"]["address"])
assert.Equal(t, 20, m1["json"]["age"].(int))
type AnotherHeader struct {
Version string `header:"version"`
}
v2 := struct {
Name string `json:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
BaseHeader
AnotherHeader
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
BaseHeader: BaseHeader{
Token: "token_xxx",
},
AnotherHeader: AnotherHeader{
Version: "v1.0",
},
}
m2, err2 := Marshal(v2)
assert.Nil(t, err2)
assert.Equal(t, "kevin", m2["json"]["name"])
assert.Equal(t, "shanghai", m2["json"]["address"])
assert.Equal(t, 20, m2["json"]["age"].(int))
assert.Equal(t, "token_xxx", m2["header"]["token"])
assert.Equal(t, "v1.0", m2["header"]["version"])
type PointerHeader struct {
Ref *string `header:"ref"`
}
ref := "reference"
v3 := struct {
Name string `json:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
PointerHeader
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
PointerHeader: PointerHeader{
Ref: &ref,
},
}
m3, err3 := Marshal(v3)
assert.Nil(t, err3)
assert.Equal(t, "kevin", m3["json"]["name"])
assert.Equal(t, "shanghai", m3["json"]["address"])
assert.Equal(t, 20, m3["json"]["age"].(int))
assert.Equal(t, "reference", *m3["header"]["ref"].(*string))
})
t.Run("bad anonymous", func(t *testing.T) {
type BaseHeader struct {
Token string `json:"token,options=[a,b]"`
}
v := struct {
Name string `json:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
BaseHeader
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
BaseHeader: BaseHeader{
Token: "c",
},
}
_, err := Marshal(v)
assert.NotNil(t, err)
})
}
func TestMarshal_Ptr(t *testing.T) {
v := &struct {
Name string `path:"name"`
@@ -344,3 +462,15 @@ func TestMarshal_FromString(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, "10", m["json"]["age"].(string))
}
func TestMarshal_Array(t *testing.T) {
v := struct {
H [1]int `json:"h,string"`
}{
H: [1]int{1},
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "[1]", m["json"]["h"].(string))
}

View File

@@ -2,10 +2,12 @@ package mapping
import (
"encoding"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"sync"
@@ -14,7 +16,6 @@ import (
"github.com/zeromicro/go-zero/core/jsonx"
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/stringx"
)
const (
@@ -50,6 +51,7 @@ type (
unmarshalOptions struct {
fillDefault bool
fromArray bool
fromString bool
opaqueKeys bool
canonicalKey func(key string) string
@@ -79,40 +81,11 @@ func (u *Unmarshaler) Unmarshal(i, v any) error {
return u.unmarshal(i, v, "")
}
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
valueType := reflect.TypeOf(v)
if valueType.Kind() != reflect.Ptr {
return errValueNotSettable
}
elemType := Deref(valueType)
switch iv := i.(type) {
case map[string]any:
if elemType.Kind() != reflect.Struct {
return errTypeMismatch
}
return u.unmarshalValuer(mapValuer(iv), v, fullName)
case []any:
if elemType.Kind() != reflect.Slice {
return errTypeMismatch
}
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default:
return errUnsupportedType
}
}
// UnmarshalValuer unmarshals m into v.
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
return u.unmarshalValuer(simpleValuer{current: m}, v, "")
}
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
}
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value,
mapValue any, fullName string) error {
if !value.CanSet() {
@@ -172,13 +145,14 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
baseType := fieldType.Elem()
dereffedBaseType := Deref(baseType)
dereffedBaseKind := dereffedBaseType.Kind()
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
if refValue.Len() == 0 {
value.Set(conv)
value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
return nil
}
var valid bool
conv := reflect.MakeSlice(reflect.SliceOf(baseType), refValue.Len(), refValue.Cap())
for i := 0; i < refValue.Len(); i++ {
ithValue := refValue.Index(i).Interface()
if ithValue == nil {
@@ -190,17 +164,9 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
switch dereffedBaseKind {
case reflect.Struct:
target := reflect.New(dereffedBaseType)
val, ok := ithValue.(map[string]any)
if !ok {
return errTypeMismatch
}
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
if err := u.fillStructElement(baseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err
}
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
case reflect.Slice:
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
return err
@@ -235,7 +201,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
return errUnsupportedType
}
baseFieldType := Deref(fieldType.Elem())
baseFieldType := fieldType.Elem()
baseFieldKind := baseFieldType.Kind()
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
@@ -256,29 +222,39 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
}
ithVal := slice.Index(index)
ithValType := ithVal.Type()
switch v := value.(type) {
case fmt.Stringer:
return setValueFromString(baseKind, ithVal, v.String())
case string:
return setValueFromString(baseKind, ithVal, v)
case map[string]any:
return u.fillMap(ithVal.Type(), ithVal, value, fullName)
// deref to handle both pointer and non-pointer types.
switch Deref(ithValType).Kind() {
case reflect.Struct:
return u.fillStructElement(ithValType, ithVal, v, fullName)
case reflect.Map:
return u.fillMap(ithValType, ithVal, value, fullName)
default:
return errTypeMismatch
}
default:
// don't need to consider the difference between int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
if ithVal.Kind() == reflect.Ptr {
baseType := Deref(ithVal.Type())
baseType := Deref(ithValType)
if !reflect.TypeOf(value).AssignableTo(baseType) {
return errTypeMismatch
}
target := reflect.New(baseType).Elem()
target.Set(reflect.ValueOf(value))
SetValue(ithVal.Type(), ithVal, target)
SetValue(ithValType, ithVal, target)
return nil
}
if !reflect.TypeOf(value).AssignableTo(ithVal.Type()) {
if !reflect.TypeOf(value).AssignableTo(ithValType) {
return errTypeMismatch
}
@@ -309,6 +285,23 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
return u.fillSlice(derefedType, value, slice, fullName)
}
func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
value any, fullName string) error {
val, ok := value.(map[string]any)
if !ok {
return errTypeMismatch
}
// use Deref(baseType) to get the base type in case the type is a pointer type.
ptr := reflect.New(Deref(baseType))
if err := u.unmarshal(val, ptr.Interface(), fullName); err != nil {
return err
}
SetValue(baseType, target, ptr.Elem())
return nil
}
func (u *Unmarshaler) fillUnmarshalerStruct(fieldType reflect.Type,
value reflect.Value, targetValue string) error {
if !value.CanSet() {
@@ -611,6 +604,22 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
case valueKind == reflect.String && typeKind == reflect.Map:
return u.fillMapFromString(value, mapValue)
case valueKind == reflect.String && typeKind == reflect.Slice:
// try to find out if it's a byte slice,
// more details https://pkg.go.dev/encoding/json#Marshal
// array and slice values encode as JSON arrays,
// except that []byte encodes as a base64-encoded string,
// and a nil slice encoded as the null JSON value.
// https://stackoverflow.com/questions/34089750/marshal-byte-to-json-giving-a-strange-string
if fieldType.Elem().Kind() == reflect.Uint8 {
// check whether string type, because the kind of some other types can be string
if strVal, ok := mapValue.(string); ok {
if decodedBytes, err := base64.StdEncoding.DecodeString(strVal); err == nil {
value.Set(reflect.ValueOf(decodedBytes))
return nil
}
}
}
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
case valueKind == reflect.String && derefedFieldType == durationType:
return fillDurationValue(fieldType, value, mapValue.(string))
@@ -811,6 +820,19 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
}
if u.opts.fromArray {
fieldKind := field.Type.Kind()
if fieldKind != reflect.Slice && fieldKind != reflect.Array {
valueKind := reflect.TypeOf(mapValue).Kind()
if valueKind == reflect.Slice || valueKind == reflect.Array {
val := reflect.ValueOf(mapValue)
if val.Len() > 0 {
mapValue = val.Index(0).Interface()
}
}
}
}
return u.processNamedFieldWithValue(field.Type, value, valueWithParent{
value: mapValue,
parent: valuer,
@@ -872,7 +894,7 @@ func (u *Unmarshaler) processNamedFieldWithValueFromString(fieldType reflect.Typ
valueKind.String())
}
if !stringx.Contains(options, checkValue) {
if !slices.Contains(options, checkValue) {
return fmt.Errorf(`value "%s" for field %q is not defined in options "%v"`,
mapValue, key, options)
}
@@ -938,6 +960,35 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
return nil
}
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
valueType := reflect.TypeOf(v)
if valueType.Kind() != reflect.Ptr {
return errValueNotSettable
}
elemType := Deref(valueType)
switch iv := i.(type) {
case map[string]any:
if elemType.Kind() != reflect.Struct {
return errTypeMismatch
}
return u.unmarshalValuer(mapValuer(iv), v, fullName)
case []any:
if elemType.Kind() != reflect.Slice {
return errTypeMismatch
}
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
default:
return errUnsupportedType
}
}
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
}
func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
rv := reflect.ValueOf(v)
if err := ValidatePtr(rv); err != nil {
@@ -990,6 +1041,16 @@ func WithDefault() UnmarshalOption {
}
}
// WithFromArray customizes an Unmarshaler with converting array values to non-array types.
// For example, if the field type is []string, and the value is [hello],
// the field type can be `string`, instead of `[]string`.
// Typically, this option is used for unmarshaling from form values.
func WithFromArray() UnmarshalOption {
return func(opt *unmarshalOptions) {
opt.fromArray = true
}
}
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
// Opaque keys are keys that are not processed by the unmarshaler.
func WithOpaqueKeys() UnmarshalOption {

View File

@@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/jsonx"
"github.com/zeromicro/go-zero/core/stringx"
)
@@ -351,7 +352,7 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
assert.Error(t, UnmarshalKey(m, &in))
})
t.Run("int slice with nil", func(t *testing.T) {
t.Run("int slice with nil element", func(t *testing.T) {
type inner struct {
Ints []int `key:"ints"`
}
@@ -365,6 +366,21 @@ func TestUnmarshalIntSliceOfPtr(t *testing.T) {
assert.Empty(t, in.Ints)
}
})
t.Run("int slice with nil", func(t *testing.T) {
type inner struct {
Ints []int `key:"ints"`
}
m := map[string]any{
"ints": []any(nil),
}
var in inner
if assert.NoError(t, UnmarshalKey(m, &in)) {
assert.Empty(t, in.Ints)
}
})
}
func TestUnmarshalIntWithDefault(t *testing.T) {
@@ -1374,20 +1390,80 @@ func TestUnmarshalWithFloatPtr(t *testing.T) {
}
func TestUnmarshalIntSlice(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
Slice []int `key:"slice"`
}
m := map[string]any{
"ages": []int{1, 2},
"slice": []any{},
}
t.Run("int slice from int", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
Slice []int `key:"slice"`
}
m := map[string]any{
"ages": []int{1, 2},
"slice": []any{},
}
ast := assert.New(t)
if ast.NoError(UnmarshalKey(m, &v)) {
ast.ElementsMatch([]int{1, 2}, v.Ages)
ast.Equal([]int{}, v.Slice)
}
ast := assert.New(t)
if ast.NoError(UnmarshalKey(m, &v)) {
ast.ElementsMatch([]int{1, 2}, v.Ages)
ast.Equal([]int{}, v.Slice)
}
})
t.Run("int slice from one int", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []int{2},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{2}, v.Ages)
}
})
t.Run("int slice from one int string", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []string{"2"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{2}, v.Ages)
}
})
t.Run("int slice from one json.Number", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []json.Number{"2"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]int{2}, v.Ages)
}
})
t.Run("int slice from one int strings", func(t *testing.T) {
var v struct {
Ages []int `key:"ages"`
}
m := map[string]any{
"ages": []string{"1,2"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
ast.Error(unmarshaler.Unmarshal(m, &v))
})
}
func TestUnmarshalString(t *testing.T) {
@@ -1442,6 +1518,51 @@ func TestUnmarshalStringSliceFromString(t *testing.T) {
}
})
t.Run("slice from empty string", func(t *testing.T) {
var v struct {
Names []string `key:"names"`
}
m := map[string]any{
"names": []string{""},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{""}, v.Names)
}
})
t.Run("slice from empty and valid string", func(t *testing.T) {
var v struct {
Names []string `key:"names"`
}
m := map[string]any{
"names": []string{","},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{","}, v.Names)
}
})
t.Run("slice from valid strings with comma", func(t *testing.T) {
var v struct {
Names []string `key:"names"`
}
m := map[string]any{
"names": []string{"aa,bb"},
}
ast := assert.New(t)
unmarshaler := NewUnmarshaler(defaultKeyName, WithFromArray())
if ast.NoError(unmarshaler.Unmarshal(m, &v)) {
ast.ElementsMatch([]string{"aa,bb"}, v.Names)
}
})
t.Run("slice from string with slice error", func(t *testing.T) {
var v struct {
Names []int `key:"names"`
@@ -4761,14 +4882,28 @@ func TestUnmarshal_EnvWithOptionsWrongValueString(t *testing.T) {
func TestUnmarshalJsonReaderMultiArray(t *testing.T) {
t.Run("reader multi array", func(t *testing.T) {
var res struct {
type testRes struct {
A string `json:"a"`
B [][]string `json:"b"`
C []byte `json:"c"`
}
payload := `{"a": "133", "b": [["add", "cccd"], ["eeee"]]}`
var res testRes
marshal := testRes{
A: "133",
B: [][]string{
{"add", "cccd"},
{"eeee"},
},
C: []byte("11122344wsss"),
}
bytes, err := jsonx.Marshal(marshal)
assert.NoError(t, err)
payload := string(bytes)
reader := strings.NewReader(payload)
if assert.NoError(t, UnmarshalJsonReader(reader, &res)) {
assert.Equal(t, 2, len(res.B))
assert.Equal(t, string(marshal.C), string(res.C))
}
})
@@ -5639,6 +5774,62 @@ func TestUnmarshalFromStringSliceForTypeMismatch(t *testing.T) {
}, &v))
}
func TestUnmarshalWithFromArray(t *testing.T) {
t.Run("array", func(t *testing.T) {
var v struct {
Value []string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo", "bar"},
}, &v)) {
assert.ElementsMatch(t, []string{"foo", "bar"}, v.Value)
}
})
t.Run("not array", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{"foo"},
}, &v)) {
assert.Equal(t, "foo", v.Value)
}
})
t.Run("not array and empty", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{
"value": []string{""},
}, &v)) {
assert.Empty(t, v.Value)
}
})
t.Run("not array and no value", func(t *testing.T) {
var v struct {
Value string `key:"value"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
assert.Error(t, unmarshaler.Unmarshal(map[string]any{}, &v))
})
t.Run("not array and no value and optional", func(t *testing.T) {
var v struct {
Value string `key:"value,optional"`
}
unmarshaler := NewUnmarshaler("key", WithFromArray())
if assert.NoError(t, unmarshaler.Unmarshal(map[string]any{}, &v)) {
assert.Empty(t, v.Value)
}
})
}
func TestUnmarshalWithOpaqueKeys(t *testing.T) {
var v struct {
Opaque string `key:"opaque.key"`
@@ -5806,6 +5997,38 @@ func TestUnmarshal_Unmarshaler(t *testing.T) {
})
}
func TestParseJsonStringValue(t *testing.T) {
t.Run("string", func(t *testing.T) {
type GoodsInfo struct {
Sku int64 `json:"sku,optional"`
}
type GetReq struct {
GoodsList []*GoodsInfo `json:"goods_list"`
}
input := map[string]any{"goods_list": "[{\"sku\":11},{\"sku\":22}]"}
var v GetReq
assert.NotPanics(t, func() {
assert.NoError(t, UnmarshalJsonMap(input, &v))
assert.Equal(t, 2, len(v.GoodsList))
assert.ElementsMatch(t, []int64{11, 22}, []int64{v.GoodsList[0].Sku, v.GoodsList[1].Sku})
})
})
t.Run("string with invalid type", func(t *testing.T) {
type GetReq struct {
GoodsList []*int `json:"goods_list"`
}
input := map[string]any{"goods_list": "[{\"sku\":11},{\"sku\":22}]"}
var v GetReq
assert.NotPanics(t, func() {
assert.Error(t, UnmarshalJsonMap(input, &v))
})
})
}
func BenchmarkDefaultValue(b *testing.B) {
for i := 0; i < b.N; i++ {
var a struct {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"reflect"
"slices"
"strconv"
"strings"
"sync"
@@ -634,11 +635,11 @@ func validateValueInOptions(val any, options []string) error {
if len(options) > 0 {
switch v := val.(type) {
case string:
if !stringx.Contains(options, v) {
if !slices.Contains(options, v) {
return fmt.Errorf(`error: value %q is not defined in options "%v"`, v, options)
}
default:
if !stringx.Contains(options, Repr(v)) {
if !slices.Contains(options, Repr(v)) {
return fmt.Errorf(`error: value "%v" is not defined in options "%v"`, val, options)
}
}

View File

@@ -1,19 +1,13 @@
package mathx
// MaxInt returns the larger one of a and b.
// Deprecated: use builtin max instead.
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
return max(a, b)
}
// MinInt returns the smaller one of a and b.
// Deprecated: use builtin min instead.
func MinInt(a, b int) int {
if a < b {
return a
}
return b
return min(a, b)
}

View File

@@ -142,89 +142,6 @@ func MapReduceChan[T, U, V any](source <-chan T, mapper MapperFunc[T, U], reduce
return mapReduceWithPanicChan(source, panicChan, mapper, reducer, opts...)
}
// mapReduceWithPanicChan maps all elements from source, and reduce the output elements with given reducer.
func mapReduceWithPanicChan[T, U, V any](source <-chan T, panicChan *onceChan, mapper MapperFunc[T, U],
reducer ReducerFunc[U, V], opts ...Option) (val V, err error) {
options := buildOptions(opts...)
// output is used to write the final result
output := make(chan V)
defer func() {
// reducer can only write once, if more, panic
for range output {
panic("more than one element written in reducer")
}
}()
// collector is used to collect data from mapper, and consume in reducer
collector := make(chan U, options.workers)
// if done is closed, all mappers and reducer should stop processing
done := make(chan struct{})
writer := newGuardedWriter(options.ctx, output, done)
var closeOnce sync.Once
// use atomic type to avoid data race
var retErr errorx.AtomicError
finish := func() {
closeOnce.Do(func() {
close(done)
close(output)
})
}
cancel := once(func(err error) {
if err != nil {
retErr.Set(err)
} else {
retErr.Set(ErrCancelWithNil)
}
drain(source)
finish()
})
go func() {
defer func() {
drain(collector)
if r := recover(); r != nil {
panicChan.write(r)
}
finish()
}()
reducer(collector, writer, cancel)
}()
go executeMappers(mapperContext[T, U]{
ctx: options.ctx,
mapper: func(item T, w Writer[U]) {
mapper(item, w, cancel)
},
source: source,
panicChan: panicChan,
collector: collector,
doneChan: done,
workers: options.workers,
})
select {
case <-options.ctx.Done():
cancel(context.DeadlineExceeded)
err = context.DeadlineExceeded
case v := <-panicChan.channel:
// drain output here, otherwise for loop panic in defer
drain(output)
panic(v)
case v, ok := <-output:
if e := retErr.Load(); e != nil {
err = e
} else if ok {
val = v
} else {
err = ErrReduceNoOutput
}
}
return
}
// MapReduceVoid maps all elements generated from given generate,
// and reduce the output elements with given reducer.
func MapReduceVoid[T, U any](generate GenerateFunc[T], mapper MapperFunc[T, U],
@@ -330,6 +247,89 @@ func executeMappers[T, U any](mCtx mapperContext[T, U]) {
}
}
// mapReduceWithPanicChan maps all elements from source, and reduce the output elements with given reducer.
func mapReduceWithPanicChan[T, U, V any](source <-chan T, panicChan *onceChan, mapper MapperFunc[T, U],
reducer ReducerFunc[U, V], opts ...Option) (val V, err error) {
options := buildOptions(opts...)
// output is used to write the final result
output := make(chan V)
defer func() {
// reducer can only write once, if more, panic
for range output {
panic("more than one element written in reducer")
}
}()
// collector is used to collect data from mapper, and consume in reducer
collector := make(chan U, options.workers)
// if done is closed, all mappers and reducer should stop processing
done := make(chan struct{})
writer := newGuardedWriter(options.ctx, output, done)
var closeOnce sync.Once
// use atomic type to avoid data race
var retErr errorx.AtomicError
finish := func() {
closeOnce.Do(func() {
close(done)
close(output)
})
}
cancel := once(func(err error) {
if err != nil {
retErr.Set(err)
} else {
retErr.Set(ErrCancelWithNil)
}
drain(source)
finish()
})
go func() {
defer func() {
drain(collector)
if r := recover(); r != nil {
panicChan.write(r)
}
finish()
}()
reducer(collector, writer, cancel)
}()
go executeMappers(mapperContext[T, U]{
ctx: options.ctx,
mapper: func(item T, w Writer[U]) {
mapper(item, w, cancel)
},
source: source,
panicChan: panicChan,
collector: collector,
doneChan: done,
workers: options.workers,
})
select {
case <-options.ctx.Done():
cancel(context.DeadlineExceeded)
err = context.DeadlineExceeded
case v := <-panicChan.channel:
// drain output here, otherwise for loop panic in defer
drain(output)
panic(v)
case v, ok := <-output:
if e := retErr.Load(); e != nil {
err = e
} else if ok {
val = v
} else {
err = ErrReduceNoOutput
}
}
return
}
func newOptions() *mapReduceOptions {
return &mapReduceOptions{
ctx: context.Background(),

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc

View File

@@ -4,6 +4,9 @@ package proc
import "time"
// ShutdownConf is empty on windows.
type ShutdownConf struct{}
// AddShutdownListener returns fn itself on windows, lets callers call fn on their own.
func AddShutdownListener(fn func()) func() {
return fn
@@ -18,6 +21,10 @@ func AddWrapUpListener(fn func()) func() {
func SetTimeToForceQuit(duration time.Duration) {
}
// Setup does nothing on windows.
func Setup(conf ShutdownConf) {
}
// Shutdown does nothing on windows.
func Shutdown() {
}

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc
@@ -14,17 +14,29 @@ import (
)
const (
wrapUpTime = time.Second
// why we use 5500 milliseconds is because most of our queue are blocking mode with 5 seconds
waitTime = 5500 * time.Millisecond
// defaultWrapUpTime is the default time to wait before calling wrap up listeners.
defaultWrapUpTime = time.Second
// defaultWaitTime is the default time to wait before force quitting.
// why we use 5500 milliseconds is because most of our queues are blocking mode with 5 seconds
defaultWaitTime = 5500 * time.Millisecond
)
var (
wrapUpListeners = new(listenerManager)
shutdownListeners = new(listenerManager)
delayTimeBeforeForceQuit = waitTime
wrapUpListeners = new(listenerManager)
shutdownListeners = new(listenerManager)
wrapUpTime = defaultWrapUpTime
waitTime = defaultWaitTime
shutdownLock sync.Mutex
)
// ShutdownConf defines the shutdown configuration for the process.
type ShutdownConf struct {
// WrapUpTime is the time to wait before calling shutdown listeners.
WrapUpTime time.Duration `json:",default=1s"`
// WaitTime is the time to wait before force quitting.
WaitTime time.Duration `json:",default=5.5s"`
}
// AddShutdownListener adds fn as a shutdown listener.
// The returned func can be used to wait for fn getting called.
func AddShutdownListener(fn func()) (waitForCalled func()) {
@@ -39,7 +51,21 @@ func AddWrapUpListener(fn func()) (waitForCalled func()) {
// SetTimeToForceQuit sets the waiting time before force quitting.
func SetTimeToForceQuit(duration time.Duration) {
delayTimeBeforeForceQuit = duration
shutdownLock.Lock()
defer shutdownLock.Unlock()
waitTime = duration
}
func Setup(conf ShutdownConf) {
shutdownLock.Lock()
defer shutdownLock.Unlock()
if conf.WrapUpTime > 0 {
wrapUpTime = conf.WrapUpTime
}
if conf.WaitTime > 0 {
waitTime = conf.WaitTime
}
}
// Shutdown calls the registered shutdown listeners, only for test purpose.
@@ -61,8 +87,12 @@ func gracefulStop(signals chan os.Signal, sig syscall.Signal) {
time.Sleep(wrapUpTime)
go shutdownListeners.notifyListeners()
time.Sleep(delayTimeBeforeForceQuit - wrapUpTime)
logx.Infof("Still alive after %v, going to force kill the process...", delayTimeBeforeForceQuit)
shutdownLock.Lock()
remainingTime := waitTime - wrapUpTime
shutdownLock.Unlock()
time.Sleep(remainingTime)
logx.Infof("Still alive after %v, going to force kill the process...", waitTime)
_ = syscall.Kill(syscall.Getpid(), sig)
}
@@ -82,6 +112,9 @@ func (lm *listenerManager) addListener(fn func()) (waitForCalled func()) {
})
lm.lock.Unlock()
// we can return lm.waitGroup.Wait directly,
// but we want to make the returned func more readable.
// creating an extra closure would be negligible in practice.
return func() {
lm.waitGroup.Wait()
}

View File

@@ -1,8 +1,9 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc
import (
"sync/atomic"
"testing"
"time"
@@ -10,8 +11,12 @@ import (
)
func TestShutdown(t *testing.T) {
t.Cleanup(restoreSettings)
SetTimeToForceQuit(time.Hour)
assert.Equal(t, time.Hour, delayTimeBeforeForceQuit)
shutdownLock.Lock()
assert.Equal(t, time.Hour, waitTime)
shutdownLock.Unlock()
var val int
called := AddWrapUpListener(func() {
@@ -29,7 +34,53 @@ func TestShutdown(t *testing.T) {
assert.Equal(t, 3, val)
}
func TestShutdownWithMultipleServices(t *testing.T) {
t.Cleanup(restoreSettings)
SetTimeToForceQuit(time.Hour)
shutdownLock.Lock()
assert.Equal(t, time.Hour, waitTime)
shutdownLock.Unlock()
var val int32
called1 := AddShutdownListener(func() {
atomic.AddInt32(&val, 1)
})
called2 := AddShutdownListener(func() {
atomic.AddInt32(&val, 2)
})
Shutdown()
called1()
called2()
assert.Equal(t, int32(3), atomic.LoadInt32(&val))
}
func TestWrapUpWithMultipleServices(t *testing.T) {
t.Cleanup(restoreSettings)
SetTimeToForceQuit(time.Hour)
shutdownLock.Lock()
assert.Equal(t, time.Hour, waitTime)
shutdownLock.Unlock()
var val int32
called1 := AddWrapUpListener(func() {
atomic.AddInt32(&val, 1)
})
called2 := AddWrapUpListener(func() {
atomic.AddInt32(&val, 2)
})
WrapUp()
called1()
called2()
assert.Equal(t, int32(3), atomic.LoadInt32(&val))
}
func TestNotifyMoreThanOnce(t *testing.T) {
t.Cleanup(restoreSettings)
ch := make(chan struct{}, 1)
go func() {
@@ -58,3 +109,38 @@ func TestNotifyMoreThanOnce(t *testing.T) {
t.Fatal("timeout, check error logs")
}
}
func TestSetup(t *testing.T) {
t.Run("valid time", func(t *testing.T) {
defer restoreSettings()
Setup(ShutdownConf{
WrapUpTime: time.Second * 2,
WaitTime: time.Second * 30,
})
shutdownLock.Lock()
assert.Equal(t, time.Second*2, wrapUpTime)
assert.Equal(t, time.Second*30, waitTime)
shutdownLock.Unlock()
})
t.Run("valid time", func(t *testing.T) {
defer restoreSettings()
Setup(ShutdownConf{})
shutdownLock.Lock()
assert.Equal(t, defaultWrapUpTime, wrapUpTime)
assert.Equal(t, defaultWaitTime, waitTime)
shutdownLock.Unlock()
})
}
func restoreSettings() {
shutdownLock.Lock()
defer shutdownLock.Unlock()
wrapUpTime = defaultWrapUpTime
waitTime = defaultWaitTime
}

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin
//go:build linux || darwin || freebsd
package proc

View File

@@ -1,13 +1,12 @@
package prof
import (
"bytes"
"strconv"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/olekukonko/tablewriter"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/threading"
)
@@ -28,46 +27,15 @@ type (
const flushInterval = 5 * time.Minute
var (
pc = &profileCenter{
slots: make(map[string]*profileSlot),
}
once sync.Once
)
func report(name string, duration time.Duration) {
updated := func() bool {
pc.lock.RLock()
defer pc.lock.RUnlock()
slot, ok := pc.slots[name]
if ok {
atomic.AddInt64(&slot.lifecount, 1)
atomic.AddInt64(&slot.lastcount, 1)
atomic.AddInt64(&slot.lifecycle, int64(duration))
atomic.AddInt64(&slot.lastcycle, int64(duration))
}
return ok
}()
if !updated {
func() {
pc.lock.Lock()
defer pc.lock.Unlock()
pc.slots[name] = &profileSlot{
lifecount: 1,
lastcount: 1,
lifecycle: int64(duration),
lastcycle: int64(duration),
}
}()
}
once.Do(flushRepeatly)
var pc = &profileCenter{
slots: make(map[string]*profileSlot),
}
func flushRepeatly() {
func init() {
flushRepeatedly()
}
func flushRepeatedly() {
threading.GoSafe(func() {
for {
time.Sleep(flushInterval)
@@ -76,42 +44,64 @@ func flushRepeatly() {
})
}
func report(name string, duration time.Duration) {
slot := loadOrStoreSlot(name, duration)
atomic.AddInt64(&slot.lifecount, 1)
atomic.AddInt64(&slot.lastcount, 1)
atomic.AddInt64(&slot.lifecycle, int64(duration))
atomic.AddInt64(&slot.lastcycle, int64(duration))
}
func loadOrStoreSlot(name string, duration time.Duration) *profileSlot {
pc.lock.RLock()
slot, ok := pc.slots[name]
pc.lock.RUnlock()
if ok {
return slot
}
pc.lock.Lock()
defer pc.lock.Unlock()
// double-check
if slot, ok = pc.slots[name]; ok {
return slot
}
slot = &profileSlot{}
pc.slots[name] = slot
return slot
}
func generateReport() string {
var buffer bytes.Buffer
buffer.WriteString("Profiling report\n")
var data [][]string
var builder strings.Builder
builder.WriteString("Profiling report\n")
builder.WriteString("QUEUE,LIFECOUNT,LIFECYCLE,LASTCOUNT,LASTCYCLE\n")
calcFn := func(total, count int64) string {
if count == 0 {
return "-"
}
return (time.Duration(total) / time.Duration(count)).String()
}
func() {
pc.lock.Lock()
defer pc.lock.Unlock()
pc.lock.Lock()
for key, slot := range pc.slots {
builder.WriteString(fmt.Sprintf("%s,%d,%s,%d,%s\n",
key,
slot.lifecount,
calcFn(slot.lifecycle, slot.lifecount),
slot.lastcount,
calcFn(slot.lastcycle, slot.lastcount),
))
for key, slot := range pc.slots {
data = append(data, []string{
key,
strconv.FormatInt(slot.lifecount, 10),
calcFn(slot.lifecycle, slot.lifecount),
strconv.FormatInt(slot.lastcount, 10),
calcFn(slot.lastcycle, slot.lastcount),
})
// reset last cycle stats
atomic.StoreInt64(&slot.lastcount, 0)
atomic.StoreInt64(&slot.lastcycle, 0)
}
pc.lock.Unlock()
// reset the data for last cycle
slot.lastcount = 0
slot.lastcycle = 0
}
}()
table := tablewriter.NewWriter(&buffer)
table.SetHeader([]string{"QUEUE", "LIFECOUNT", "LIFECYCLE", "LASTCOUNT", "LASTCYCLE"})
table.SetBorder(false)
table.AppendBulk(data)
table.Render()
return buffer.String()
return builder.String()
}

View File

@@ -8,7 +8,6 @@ import (
)
func TestReport(t *testing.T) {
once.Do(func() {})
assert.NotContains(t, generateReport(), "foo")
report("foo", time.Second)
assert.Contains(t, generateReport(), "foo")

View File

@@ -8,6 +8,7 @@ import (
"github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/trace"
"github.com/zeromicro/go-zero/internal/devserver"
"github.com/zeromicro/go-zero/internal/profiling"
)
const (
@@ -37,6 +38,9 @@ type (
Prometheus prometheus.Config `json:",optional"`
Telemetry trace.Config `json:",optional"`
DevServer DevServerConfig `json:",optional"`
Shutdown proc.ShutdownConf `json:",optional"`
// Profiling is the configuration for continuous profiling.
Profiling profiling.Config `json:",optional"`
}
)
@@ -61,6 +65,7 @@ func (sc ServiceConf) SetUp() error {
sc.Telemetry.Name = sc.Name
}
trace.StartAgent(sc.Telemetry)
proc.Setup(sc.Shutdown)
proc.AddShutdownListener(func() {
trace.StopAgent()
})
@@ -68,7 +73,9 @@ func (sc ServiceConf) SetUp() error {
if len(sc.MetricsUrl) > 0 {
stat.SetReportWriter(stat.NewRemoteWriter(sc.MetricsUrl))
}
devserver.StartAgent(sc.DevServer)
profiling.Start(sc.Profiling)
return nil
}

View File

@@ -1,9 +1,10 @@
package service
import (
"sync"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/threading"
)
@@ -35,7 +36,7 @@ type (
// NewServiceGroup returns a ServiceGroup.
func NewServiceGroup() *ServiceGroup {
sg := new(ServiceGroup)
sg.stopOnce = syncx.Once(sg.doStop)
sg.stopOnce = sync.OnceFunc(sg.doStop)
return sg
}
@@ -76,9 +77,14 @@ func (sg *ServiceGroup) doStart() {
}
func (sg *ServiceGroup) doStop() {
group := threading.NewRoutineGroup()
for _, service := range sg.services {
service.Stop()
// new variable to avoid closure problems, can be removed after go 1.22
// see https://golang.org/doc/faq#closures_and_goroutines
service := service
group.Run(service.Stop)
}
group.Wait()
}
// WithStart wraps a start func as a Service.

View File

@@ -19,7 +19,6 @@ import (
const (
clusterNameKey = "CLUSTER_NAME"
testEnv = "test.v"
timeFormat = "2006-01-02 15:04:05"
)
var (
@@ -45,7 +44,7 @@ func Report(msg string) {
if fn != nil {
reported := lessExecutor.DoOrDiscard(func() {
var builder strings.Builder
builder.WriteString(fmt.Sprintln(time.Now().Format(timeFormat)))
builder.WriteString(fmt.Sprintln(time.Now().Format(time.DateTime)))
if len(clusterName) > 0 {
builder.WriteString(fmt.Sprintf("cluster: %s\n", clusterName))
}

View File

@@ -19,6 +19,7 @@ type (
RedisConf struct {
Host string
Type string `json:",default=node,options=node|cluster"`
User string `json:",optional"`
Pass string `json:",optional"`
Tls bool `json:",optional"`
NonBlock bool `json:",default=true"`
@@ -40,6 +41,9 @@ func (rc RedisConf) NewRedis() *Redis {
if rc.Type == ClusterType {
opts = append(opts, Cluster())
}
if len(rc.User) > 0 {
opts = append(opts, WithUser(rc.User))
}
if len(rc.Pass) > 0 {
opts = append(opts, WithPass(rc.Pass))
}

View File

@@ -55,6 +55,7 @@ type (
Redis struct {
Addr string
Type string
User string
Pass string
tls bool
brk breaker.Breaker
@@ -126,6 +127,9 @@ func NewRedis(conf RedisConf, opts ...Option) (*Redis, error) {
if conf.Type == ClusterType {
opts = append([]Option{Cluster()}, opts...)
}
if len(conf.User) > 0 {
opts = append([]Option{WithUser(conf.User)}, opts...)
}
if len(conf.Pass) > 0 {
opts = append([]Option{WithPass(conf.Pass)}, opts...)
}
@@ -605,6 +609,28 @@ func (s *Redis) GetBitCtx(ctx context.Context, key string, offset int64) (int, e
return int(v), nil
}
// GetDel is the implementation of redis getdel command.
// Available since: redis version 6.2.0
func (s *Redis) GetDel(key string) (string, error) {
return s.GetDelCtx(context.Background(), key)
}
// GetDelCtx is the implementation of redis getdel command.
// Available since: redis version 6.2.0
func (s *Redis) GetDelCtx(ctx context.Context, key string) (string, error) {
conn, err := getRedis(s)
if err != nil {
return "", err
}
val, err := conn.GetDel(ctx, key).Result()
if errors.Is(err, red.Nil) {
return "", nil
}
return val, err
}
// GetSet is the implementation of redis getset command.
func (s *Redis) GetSet(key, value string) (string, error) {
return s.GetSetCtx(context.Background(), key, value)
@@ -1197,6 +1223,18 @@ func (s *Redis) PipelinedCtx(ctx context.Context, fn func(Pipeliner) error) erro
return err
}
func (s *Redis) Publish(channel string, message interface{}) (int64, error) {
return s.PublishCtx(context.Background(), channel, message)
}
func (s *Redis) PublishCtx(ctx context.Context, channel string, message interface{}) (int64, error) {
conn, err := getRedis(s)
if err != nil {
return 0, err
}
return conn.Publish(ctx, channel, message).Result()
}
// Rpop is the implementation of redis rpop command.
func (s *Redis) Rpop(key string) (string, error) {
return s.RpopCtx(context.Background(), key)
@@ -1247,6 +1285,18 @@ func (s *Redis) RpushCtx(ctx context.Context, key string, values ...any) (int, e
return int(v), nil
}
func (s *Redis) RPopLPush(source string, destination string) (string, error) {
return s.RPopLPushCtx(context.Background(), source, destination)
}
func (s *Redis) RPopLPushCtx(ctx context.Context, source string, destination string) (string, error) {
conn, err := getRedis(s)
if err != nil {
return "", err
}
return conn.RPopLPush(ctx, source, destination).Result()
}
// Sadd is the implementation of redis sadd command.
func (s *Redis) Sadd(key string, values ...any) (int, error) {
return s.SaddCtx(context.Background(), key, values...)
@@ -1645,6 +1695,26 @@ func (s *Redis) TtlCtx(ctx context.Context, key string) (int, error) {
return int(duration), nil
}
func (s *Redis) TxPipeline() (pipe Pipeliner, err error) {
conn, err := getRedis(s)
if err != nil {
return nil, err
}
return conn.TxPipeline(), nil
}
func (s *Redis) Unlink(keys ...string) (int64, error) {
return s.UnlinkCtx(context.Background(), keys...)
}
func (s *Redis) UnlinkCtx(ctx context.Context, keys ...string) (int64, error) {
conn, err := getRedis(s)
if err != nil {
return 0, err
}
return conn.Unlink(ctx, keys...).Result()
}
// Zadd is the implementation of redis zadd command.
func (s *Redis) Zadd(key string, score int64, value string) (bool, error) {
return s.ZaddCtx(context.Background(), key, score, value)
@@ -2361,6 +2431,13 @@ func SetSlowThreshold(threshold time.Duration) {
slowThreshold.Set(threshold)
}
// WithHook customizes the given Redis with given durationHook.
func WithHook(hook Hook) Option {
return func(r *Redis) {
r.hooks = append(r.hooks, hook)
}
}
// WithPass customizes the given Redis with given password.
func WithPass(pass string) Option {
return func(r *Redis) {
@@ -2375,11 +2452,10 @@ func WithTLS() Option {
}
}
// WithHook customizes the given Redis with given durationHook, only for private use now,
// maybe expose later.
func WithHook(hook Hook) Option {
// WithUser customizes the given Redis with given username.
func WithUser(user string) Option {
return func(r *Redis) {
r.hooks = append(r.hooks, hook)
r.User = user
}
}

View File

@@ -1071,6 +1071,34 @@ func TestRedis_Set(t *testing.T) {
})
}
func TestRedis_GetDel(t *testing.T) {
t.Run("get_del", func(t *testing.T) {
runOnRedis(t, func(client *Redis) {
val, err := newRedis(client.Addr).GetDel("hello")
assert.Equal(t, "", val)
assert.Nil(t, err)
err = client.Set("hello", "world")
assert.Nil(t, err)
val, err = client.Get("hello")
assert.Nil(t, err)
assert.Equal(t, "world", val)
val, err = client.GetDel("hello")
assert.Nil(t, err)
assert.Equal(t, "world", val)
val, err = client.Get("hello")
assert.Nil(t, err)
assert.Equal(t, "", val)
})
})
t.Run("get_del_with_error", func(t *testing.T) {
runOnRedisWithError(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).GetDel("hello")
assert.Error(t, err)
})
})
}
func TestRedis_GetSet(t *testing.T) {
t.Run("set_get", func(t *testing.T) {
runOnRedis(t, func(client *Redis) {
@@ -1996,9 +2024,9 @@ func TestSetSlowThreshold(t *testing.T) {
assert.Equal(t, time.Second, slowThreshold.Load())
}
func TestRedis_WithPass(t *testing.T) {
func TestRedis_WithUserPass(t *testing.T) {
runOnRedis(t, func(client *Redis) {
err := newRedis(client.Addr, WithPass("any")).Ping()
err := newRedis(client.Addr, WithUser("any"), WithPass("any")).Ping()
assert.NotNil(t, err)
})
}
@@ -2080,3 +2108,70 @@ func (n mockedNode) BLPop(_ context.Context, _ time.Duration, _ ...string) *red.
return cmd
}
func TestRedisPublish(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).Publish("Test", "message")
assert.NotNil(t, err)
_, err = client.Publish("Test", "message")
assert.Nil(t, err)
})
}
func TestRedisRPopLPush(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).RPopLPush("Source", "Destination")
assert.NotNil(t, err)
_, err = client.Rpush("Source", "Destination")
assert.Nil(t, err)
_, err = client.RPopLPush("Source", "Destination")
assert.Nil(t, err)
})
}
func TestRedisUnlink(t *testing.T) {
runOnRedis(t, func(client *Redis) {
_, err := newRedis(client.Addr, badType()).Unlink("Key1", "Key2")
assert.NotNil(t, err)
err = client.Set("Key1", "Key2")
assert.Nil(t, err)
get, err := client.Get("Key1")
assert.Nil(t, err)
assert.Equal(t, "Key2", get)
res, err := client.Unlink("Key1")
assert.Nil(t, err)
assert.Equal(t, int64(1), res)
})
}
func TestRedisTxPipeline(t *testing.T) {
runOnRedis(t, func(client *Redis) {
ctx := context.Background()
_, err := newRedis(client.Addr, badType()).TxPipeline()
assert.NotNil(t, err)
pipe, err := client.TxPipeline()
assert.Nil(t, err)
key := "key"
hashKey := "field"
hashValue := "value"
// setting value
pipe.HSet(ctx, key, hashKey, hashValue)
existsCmd := pipe.Exists(ctx, key)
getCmd := pipe.HGet(ctx, key, hashKey)
// execution
_, err = pipe.Exec(ctx)
assert.Nil(t, err)
// verification results
exists, err := existsCmd.Result()
assert.Nil(t, err)
assert.Equal(t, int64(1), exists)
value, err := getCmd.Result()
assert.Nil(t, err)
assert.Equal(t, hashValue, value)
})
}

View File

@@ -21,6 +21,7 @@ func CreateBlockingNode(r *Redis) (ClosableNode, error) {
case NodeType:
client := red.NewClient(&red.Options{
Addr: r.Addr,
Username: r.User,
Password: r.Pass,
DB: defaultDatabase,
MaxRetries: maxRetries,
@@ -32,6 +33,7 @@ func CreateBlockingNode(r *Redis) (ClosableNode, error) {
case ClusterType:
client := red.NewClusterClient(&red.ClusterOptions{
Addrs: splitClusterAddrs(r.Addr),
Username: r.User,
Password: r.Pass,
MaxRetries: maxRetries,
PoolSize: 1,

View File

@@ -31,6 +31,7 @@ func getClient(r *Redis) (*red.Client, error) {
}
store := red.NewClient(&red.Options{
Addr: r.Addr,
Username: r.User,
Password: r.Pass,
DB: defaultDatabase,
MaxRetries: maxRetries,

View File

@@ -28,6 +28,7 @@ func getCluster(r *Redis) (*red.ClusterClient, error) {
}
store := red.NewClusterClient(&red.ClusterOptions{
Addrs: splitClusterAddrs(r.Addr),
Username: r.User,
Password: r.Pass,
MaxRetries: maxRetries,
MinIdleConns: idleConns,

View File

@@ -41,6 +41,7 @@ var (
type (
statGetter struct {
host string
dbName string
hash string
poolStats func() sql.DBStats

View File

@@ -267,6 +267,20 @@ func TestUnmarshalRowStruct(t *testing.T) {
}, "select name, age from users where user=?", "anyone"), ErrNotMatchDestination)
})
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
value := new(struct {
Name string
age int
})
rs := sqlmock.NewRows([]string{"name", "age"}).FromCSVString("liao,5")
mock.ExpectQuery("select (.+) from users where user=?").WithArgs("anyone").WillReturnRows(rs)
assert.ErrorIs(t, query(context.Background(), db, func(rows *sql.Rows) error {
return unmarshalRow(value, rows, true)
}, "select name, age from users where user=?", "anyone"), ErrNotMatchDestination)
})
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
rs := sqlmock.NewRows([]string{"value"}).FromCSVString("8")
mock.ExpectQuery("select (.+) from users where user=?").WithArgs("anyone").WillReturnRows(rs)
@@ -310,6 +324,20 @@ func TestUnmarshalRowStructWithTags(t *testing.T) {
}, "select name, age from users where user=?", "anyone"), ErrNotReadableValue)
})
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
value := new(struct {
age int `db:"age"`
Name string `db:"name"`
})
rs := sqlmock.NewRows([]string{"name", "age"}).FromCSVString("liao,5")
mock.ExpectQuery("select (.+) from users where user=?").WithArgs("anyone").WillReturnRows(rs)
assert.ErrorIs(t, query(context.Background(), db, func(rows *sql.Rows) error {
return unmarshalRow(value, rows, true)
}, "select name, age from users where user=?", "anyone"), ErrNotReadableValue)
})
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
var value struct {
Age *int `db:"age"`
@@ -1307,25 +1335,25 @@ func TestAnonymousStructPr(t *testing.T) {
}
func TestAnonymousStructPrError(t *testing.T) {
type Score struct {
Discipline string `db:"discipline"`
score uint `db:"score"`
}
type ClassType struct {
Grade sql.NullString `db:"grade"`
ClassName *string `db:"class_name"`
}
type Class struct {
*ClassType
Score
}
var value []*struct {
Age int64 `db:"age"`
Class
Name string `db:"name"`
}
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
type Score struct {
Discipline string `db:"discipline"`
score uint `db:"score"`
}
type ClassType struct {
Grade sql.NullString `db:"grade"`
ClassName *string `db:"class_name"`
}
type Class struct {
*ClassType
Score
}
var value []*struct {
Age int64 `db:"age"`
Class
Name string `db:"name"`
}
rs := sqlmock.NewRows([]string{
"name",
"age",
@@ -1338,10 +1366,50 @@ func TestAnonymousStructPrError(t *testing.T) {
AddRow("second", 3, "grade one", "chinese", "class three grade two", 99)
mock.ExpectQuery("select (.+) from users where user=?").
WithArgs("anyone").WillReturnRows(rs)
assert.Error(t, query(context.Background(), db, func(rows *sql.Rows) error {
assert.ErrorIs(t, query(context.Background(), db, func(rows *sql.Rows) error {
return unmarshalRows(&value, rows, true)
}, "select name, age, grade, discipline, class_name, score from users where user=?",
"anyone"))
"anyone"), ErrNotReadableValue)
if len(value) > 0 {
assert.Equal(t, value[0].score, 0)
}
})
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
type Score struct {
Discipline string
score uint
}
type ClassType struct {
Grade sql.NullString
ClassName *string
}
type Class struct {
*ClassType
Score
}
var value []*struct {
Age int64
Class
Name string
}
rs := sqlmock.NewRows([]string{
"name",
"age",
"grade",
"discipline",
"class_name",
"score",
}).
AddRow("first", 2, nil, "math", "experimental class", 100).
AddRow("second", 3, "grade one", "chinese", "class three grade two", 99)
mock.ExpectQuery("select (.+) from users where user=?").
WithArgs("anyone").WillReturnRows(rs)
assert.ErrorIs(t, query(context.Background(), db, func(rows *sql.Rows) error {
return unmarshalRows(&value, rows, true)
}, "select name, age, grade, discipline, class_name, score from users where user=?",
"anyone"), ErrNotMatchDestination)
if len(value) > 0 {
assert.Equal(t, value[0].score, 0)
}

View File

@@ -1,10 +1,14 @@
package sqlx
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"io"
"time"
"github.com/go-sql-driver/mysql"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/syncx"
)
@@ -23,6 +27,23 @@ func getCachedSqlConn(driverName, server string) (*sql.DB, error) {
return nil, err
}
if driverName != mysqlDriverName {
if cfg, e := mysql.ParseDSN(server); e != nil {
// if cannot parse, don't collect the metrics
logx.Error(e)
} else {
checksum := sha256.Sum256([]byte(server))
connCollector.registerClient(&statGetter{
host: cfg.Addr,
dbName: cfg.DBName,
hash: hex.EncodeToString(checksum[:]),
poolStats: func() sql.DBStats {
return conn.Stats()
},
})
}
}
return conn, nil
})
if err != nil {

View File

@@ -76,7 +76,7 @@ func FuzzNodeFind(f *testing.F) {
fmt.Fprintf(&buf, "text:\n\t%s\n", str)
defer func() {
if r := recover(); r != nil {
t.Errorf(buf.String())
t.Error(buf.String())
}
}()
assert.ElementsMatchf(t, scopes, n.find([]rune(str)), buf.String())

View File

@@ -2,6 +2,7 @@ package stringx
import (
"errors"
"slices"
"unicode"
"github.com/zeromicro/go-zero/core/lang"
@@ -15,14 +16,9 @@ var (
)
// Contains checks if str is in list.
// Deprecated: use slices.Contains instead.
func Contains(list []string, str string) bool {
for _, each := range list {
if each == str {
return true
}
}
return false
return slices.Contains(list, str)
}
// Filter filters chars from s with given filter function.
@@ -123,11 +119,7 @@ func Remove(strings []string, strs ...string) []string {
// Reverse reverses s.
func Reverse(s string) string {
runes := []rune(s)
for from, to := 0, len(runes)-1; from < to; from, to = from+1, to-1 {
runes[from], runes[to] = runes[to], runes[from]
}
slices.Reverse(runes)
return string(runes)
}

View File

@@ -7,6 +7,28 @@ import (
"github.com/stretchr/testify/assert"
)
func TestContainsString(t *testing.T) {
cases := []struct {
slice []string
value string
expect bool
}{
{[]string{"1"}, "1", true},
{[]string{"1"}, "2", false},
{[]string{"1", "2"}, "1", true},
{[]string{"1", "2"}, "3", false},
{nil, "3", false},
{nil, "", false},
}
for _, each := range cases {
t.Run(path.Join(each.slice...), func(t *testing.T) {
actual := Contains(each.slice, each.value)
assert.Equal(t, each.expect, actual)
})
}
}
func TestNotEmpty(t *testing.T) {
cases := []struct {
args []string
@@ -41,28 +63,6 @@ func TestNotEmpty(t *testing.T) {
}
}
func TestContainsString(t *testing.T) {
cases := []struct {
slice []string
value string
expect bool
}{
{[]string{"1"}, "1", true},
{[]string{"1"}, "2", false},
{[]string{"1", "2"}, "1", true},
{[]string{"1", "2"}, "3", false},
{nil, "3", false},
{nil, "", false},
}
for _, each := range cases {
t.Run(path.Join(each.slice...), func(t *testing.T) {
actual := Contains(each.slice, each.value)
assert.Equal(t, each.expect, actual)
})
}
}
func TestFilter(t *testing.T) {
cases := []struct {
input string

View File

@@ -3,9 +3,7 @@ package syncx
import "sync"
// Once returns a func that guarantees fn can only called once.
// Deprecated: use sync.OnceFunc instead.
func Once(fn func()) func() {
once := new(sync.Once)
return func() {
once.Do(fn)
}
return sync.OnceFunc(fn)
}

View File

@@ -5,6 +5,7 @@ import (
"runtime"
"sync"
"sync/atomic"
"time"
)
const factor = 10
@@ -100,6 +101,6 @@ func (r *StableRunner[I, O]) Wait() {
close(r.done)
r.runner.Wait()
for atomic.LoadUint64(&r.consumedIndex) < atomic.LoadUint64(&r.writtenIndex) {
runtime.Gosched()
time.Sleep(time.Millisecond)
}
}

View File

@@ -1,10 +1,10 @@
package utils
import (
"cmp"
"strconv"
"strings"
"github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/stringx"
)
@@ -39,7 +39,7 @@ func compare(v1, v2 string) int {
fields1, fields2 := strings.Split(v1, "."), strings.Split(v2, ".")
ver1, ver2 := strsToInts(fields1), strsToInts(fields2)
ver1len, ver2len := len(ver1), len(ver2)
shorter := mathx.MinInt(ver1len, ver2len)
shorter := min(ver1len, ver2len)
for i := 0; i < shorter; i++ {
if ver1[i] == ver2[i] {
@@ -50,14 +50,7 @@ func compare(v1, v2 string) int {
return 1
}
}
if ver1len < ver2len {
return -1
} else if ver1len == ver2len {
return 0
} else {
return 1
}
return cmp.Compare(ver1len, ver2len)
}
func strsToInts(strs []string) []int64 {

View File

@@ -12,14 +12,22 @@ type (
Upstreams []Upstream
}
// HttpClientConf is the configuration for an HTTP client.
HttpClientConf struct {
Target string
Prefix string `json:",optional"`
Timeout int64 `json:",default=3000"`
}
// RouteMapping is a mapping between a gateway route and an upstream rpc method.
RouteMapping struct {
// Method is the HTTP method, like GET, POST, PUT, DELETE.
Method string
// Path is the HTTP path.
Path string
// RpcPath is the gRPC rpc method, with format of package.service/method
RpcPath string
// RpcPath is the gRPC rpc method, with format of package.service/method, optional.
// If the mapping is for HTTP, it's not necessary.
RpcPath string `json:",optional"`
}
// Upstream is the configuration for an upstream.
@@ -27,12 +35,14 @@ type (
// Name is the name of the upstream.
Name string `json:",optional"`
// Grpc is the target of the upstream.
Grpc zrpc.RpcClientConf
Grpc *zrpc.RpcClientConf `json:",optional"`
// Http is the target of the upstream.
Http *HttpClientConf `json:",optional=!grpc"`
// ProtoSets is the file list of proto set, like [hello.pb].
// if your proto file import another proto file, you need to write multi-file slice,
// like [hello.pb, common.pb].
ProtoSets []string `json:",optional"`
// Mappings is the mapping between gateway routes and Upstream rpc methods.
// Mappings is the mapping between gateway routes and Upstream methods.
// Keep it blank if annotations are added in rpc methods.
Mappings []RouteMapping `json:",optional"`
}

View File

@@ -3,26 +3,35 @@ package gateway
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/fullstorydev/grpcurl"
"github.com/golang/protobuf/jsonpb"
"github.com/jhump/protoreflect/grpcreflect"
"github.com/zeromicro/go-zero/core/logc"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/mr"
"github.com/zeromicro/go-zero/core/threading"
"github.com/zeromicro/go-zero/gateway/internal"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpc"
"github.com/zeromicro/go-zero/rest/httpx"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc/codes"
)
const defaultHttpScheme = "http"
type (
// Server is a gateway server.
Server struct {
*rest.Server
upstreams []Upstream
conns []zrpc.Client
processHeader func(http.Header) []string
dialer func(conf zrpc.RpcClientConf) zrpc.Client
}
@@ -51,8 +60,24 @@ func (s *Server) Start() {
}
// Stop stops the gateway server.
// To get a graceful shutdown, it stops the HTTP server first, then closes gRPC connections.
func (s *Server) Stop() {
// stop the HTTP server first, then close gRPC connections.
// in case the gRPC server is stopped first,
// the HTTP server may still be running to accept requests.
s.Server.Stop()
group := threading.NewRoutineGroup()
for _, conn := range s.conns {
// new variable to avoid closure problems, can be removed after go 1.22
// see https://golang.org/doc/faq#closures_and_goroutines
conn := conn
group.Run(func() {
// ignore the error when closing the connection
_ = conn.Conn().Close()
})
}
group.Wait()
}
func (s *Server) build() error {
@@ -65,51 +90,11 @@ func (s *Server) build() error {
source <- up
}
}, func(up Upstream, writer mr.Writer[rest.Route], cancel func(error)) {
var cli zrpc.Client
if s.dialer != nil {
cli = s.dialer(up.Grpc)
} else {
cli = zrpc.MustNewClient(up.Grpc)
}
source, err := s.createDescriptorSource(cli, up)
if err != nil {
cancel(fmt.Errorf("%s: %w", up.Name, err))
return
}
methods, err := internal.GetMethods(source)
if err != nil {
cancel(fmt.Errorf("%s: %w", up.Name, err))
return
}
resolver := grpcurl.AnyResolverFromDescriptorSource(source)
for _, m := range methods {
if len(m.HttpMethod) > 0 && len(m.HttpPath) > 0 {
writer.Write(rest.Route{
Method: m.HttpMethod,
Path: m.HttpPath,
Handler: s.buildHandler(source, resolver, cli, m.RpcPath),
})
}
}
methodSet := make(map[string]struct{})
for _, m := range methods {
methodSet[m.RpcPath] = struct{}{}
}
for _, m := range up.Mappings {
if _, ok := methodSet[m.RpcPath]; !ok {
cancel(fmt.Errorf("%s: rpc method %s not found", up.Name, m.RpcPath))
return
}
writer.Write(rest.Route{
Method: strings.ToUpper(m.Method),
Path: m.Path,
Handler: s.buildHandler(source, resolver, cli, m.RpcPath),
})
// up.Grpc and up.Http are exclusive
if up.Grpc != nil {
s.buildGrpcRoute(up, writer, cancel)
} else if up.Http != nil {
s.buildHttpRoute(up, writer)
}
}, func(pipe <-chan rest.Route, cancel func(error)) {
for route := range pipe {
@@ -118,7 +103,7 @@ func (s *Server) build() error {
})
}
func (s *Server) buildHandler(source grpcurl.DescriptorSource, resolver jsonpb.AnyResolver,
func (s *Server) buildGrpcHandler(source grpcurl.DescriptorSource, resolver jsonpb.AnyResolver,
cli zrpc.Client, rpcPath string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
parser, err := internal.NewRequestParser(r, resolver)
@@ -141,31 +126,121 @@ func (s *Server) buildHandler(source grpcurl.DescriptorSource, resolver jsonpb.A
}
}
func (s *Server) createDescriptorSource(cli zrpc.Client, up Upstream) (grpcurl.DescriptorSource, error) {
var source grpcurl.DescriptorSource
var err error
if len(up.ProtoSets) > 0 {
source, err = grpcurl.DescriptorSourceFromProtoSets(up.ProtoSets...)
if err != nil {
return nil, err
}
func (s *Server) buildGrpcRoute(up Upstream, writer mr.Writer[rest.Route], cancel func(error)) {
var cli zrpc.Client
if s.dialer != nil {
cli = s.dialer(*up.Grpc)
} else {
client := grpcreflect.NewClientAuto(context.Background(), cli.Conn())
source = grpcurl.DescriptorSourceFromServer(context.Background(), client)
cli = zrpc.MustNewClient(*up.Grpc)
}
s.conns = append(s.conns, cli)
source, err := createDescriptorSource(cli, up)
if err != nil {
cancel(fmt.Errorf("%s: %w", up.Name, err))
return
}
return source, nil
methods, err := internal.GetMethods(source)
if err != nil {
cancel(fmt.Errorf("%s: %w", up.Name, err))
return
}
resolver := grpcurl.AnyResolverFromDescriptorSource(source)
for _, m := range methods {
if len(m.HttpMethod) > 0 && len(m.HttpPath) > 0 {
writer.Write(rest.Route{
Method: m.HttpMethod,
Path: m.HttpPath,
Handler: s.buildGrpcHandler(source, resolver, cli, m.RpcPath),
})
}
}
methodSet := make(map[string]struct{})
for _, m := range methods {
methodSet[m.RpcPath] = struct{}{}
}
for _, m := range up.Mappings {
if _, ok := methodSet[m.RpcPath]; !ok {
cancel(fmt.Errorf("%s: rpc method %s not found", up.Name, m.RpcPath))
return
}
writer.Write(rest.Route{
Method: strings.ToUpper(m.Method),
Path: m.Path,
Handler: s.buildGrpcHandler(source, resolver, cli, m.RpcPath),
})
}
}
func (s *Server) buildHttpHandler(target *HttpClientConf) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(httpx.ContentType, httpx.JsonContentType)
req, err := buildRequestWithNewTarget(r, target)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
// set the timeout if it's configured, take effect only if it's greater than 0
// and less than the deadline of the original request
if target.Timeout > 0 {
timeout := time.Duration(target.Timeout) * time.Millisecond
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
req = req.WithContext(ctx)
}
resp, err := httpc.DoRequest(req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
defer resp.Body.Close()
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(resp.StatusCode)
if _, err = io.Copy(w, resp.Body); err != nil {
// log the error with original request info
logc.Error(r.Context(), err)
}
}
}
func (s *Server) buildHttpRoute(up Upstream, writer mr.Writer[rest.Route]) {
for _, m := range up.Mappings {
writer.Write(rest.Route{
Method: strings.ToUpper(m.Method),
Path: m.Path,
Handler: s.buildHttpHandler(up.Http),
})
}
}
func (s *Server) ensureUpstreamNames() error {
for i := 0; i < len(s.upstreams); i++ {
target, err := s.upstreams[i].Grpc.BuildTarget()
if err != nil {
return err
if len(s.upstreams[i].Name) > 0 {
continue
}
s.upstreams[i].Name = target
if s.upstreams[i].Grpc != nil {
target, err := s.upstreams[i].Grpc.BuildTarget()
if err != nil {
return err
}
s.upstreams[i].Name = target
} else if s.upstreams[i].Http != nil {
s.upstreams[i].Name = s.upstreams[i].Http.Target
}
}
return nil
@@ -188,6 +263,53 @@ func WithHeaderProcessor(processHeader func(http.Header) []string) func(*Server)
}
}
func buildRequestWithNewTarget(r *http.Request, target *HttpClientConf) (*http.Request, error) {
u := *r.URL
u.Host = target.Target
if len(u.Scheme) == 0 {
u.Scheme = defaultHttpScheme
}
if len(target.Prefix) > 0 {
var err error
u.Path, err = url.JoinPath(target.Prefix, u.Path)
if err != nil {
return nil, err
}
}
newReq := &http.Request{
Method: r.Method,
URL: &u,
Header: r.Header.Clone(),
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
ContentLength: r.ContentLength,
Body: io.NopCloser(r.Body),
}
// make sure the context is passed to the new request
return newReq.WithContext(r.Context()), nil
}
func createDescriptorSource(cli zrpc.Client, up Upstream) (grpcurl.DescriptorSource, error) {
var source grpcurl.DescriptorSource
var err error
if len(up.ProtoSets) > 0 {
source, err = grpcurl.DescriptorSourceFromProtoSets(up.ProtoSets...)
if err != nil {
return nil, err
}
} else {
client := grpcreflect.NewClientAuto(context.Background(), cli.Conn())
source = grpcurl.DescriptorSourceFromServer(context.Background(), client)
}
return source, nil
}
// withDialer sets a dialer to create a gRPC client.
func withDialer(dialer func(conf zrpc.RpcClientConf) zrpc.Client) func(*Server) {
return func(s *Server) {

View File

@@ -2,9 +2,12 @@ package gateway
import (
"context"
"errors"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
@@ -46,7 +49,7 @@ func dialer() func(context.Context, string) (net.Conn, error) {
func TestMustNewServer(t *testing.T) {
var c GatewayConf
assert.NoError(t, conf.FillDefault(&c))
// avoid popup alert on macos for asking permissions
// avoid popup alert on MacOS for asking permissions
c.DevServer.Host = "localhost"
c.Host = "localhost"
c.Port = 18881
@@ -65,7 +68,7 @@ func TestMustNewServer(t *testing.T) {
RpcPath: "mock.DepositService/Deposit",
},
},
Grpc: zrpc.RpcClientConf{
Grpc: &zrpc.RpcClientConf{
Endpoints: []string{"foo"},
Timeout: 1000,
Middlewares: zrpc.ClientMiddlewaresConf{
@@ -98,7 +101,7 @@ func TestServer_ensureUpstreamNames(t *testing.T) {
var s = Server{
upstreams: []Upstream{
{
Grpc: zrpc.RpcClientConf{
Grpc: &zrpc.RpcClientConf{
Target: "target",
},
},
@@ -113,7 +116,7 @@ func TestServer_ensureUpstreamNames_badEtcd(t *testing.T) {
var s = Server{
upstreams: []Upstream{
{
Grpc: zrpc.RpcClientConf{
Grpc: &zrpc.RpcClientConf{
Etcd: discov.EtcdConf{},
},
},
@@ -125,3 +128,200 @@ func TestServer_ensureUpstreamNames_badEtcd(t *testing.T) {
s.Start()
})
}
func TestHttpToHttp(t *testing.T) {
server := startTestServer(t)
defer server.Close()
var c GatewayConf
assert.NoError(t, conf.FillDefault(&c))
c.DevServer.Host = "localhost"
c.Host = "localhost"
c.Port = 18882
s := MustNewServer(c)
s.upstreams = []Upstream{
{
Name: "test",
Mappings: []RouteMapping{
{
Method: "get",
Path: "/api/ping",
},
},
Http: &HttpClientConf{
Target: "localhost:45678",
Timeout: 3000,
},
},
{
Mappings: []RouteMapping{
{
Method: "get",
Path: "/ping",
},
},
Http: &HttpClientConf{
Target: "localhost:45678",
Prefix: "/api",
},
},
}
go s.Start()
defer s.Stop()
time.Sleep(time.Millisecond * 200)
t.Run("/api/ping", func(t *testing.T) {
resp, err := httpc.Do(context.Background(), http.MethodGet,
"http://localhost:18882/api/ping", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if assert.NoError(t, err) {
assert.Equal(t, "pong", string(body))
}
})
t.Run("/ping", func(t *testing.T) {
resp, err := httpc.Do(context.Background(), http.MethodGet,
"http://localhost:18882/ping", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if assert.NoError(t, err) {
assert.Equal(t, "pong", string(body))
}
})
t.Run("no upstream", func(t *testing.T) {
resp, err := httpc.Do(context.Background(), http.MethodGet,
"http://localhost:18882/ping/bad", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("method not allowed", func(t *testing.T) {
resp, err := httpc.Do(context.Background(), http.MethodPost,
"http://localhost:18882/api/ping", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
})
}
func TestHttpToHttpBadUpstream(t *testing.T) {
var c GatewayConf
assert.NoError(t, conf.FillDefault(&c))
c.DevServer.Host = "localhost"
c.Host = "localhost"
c.Port = 18883
s := MustNewServer(c)
s.upstreams = []Upstream{
{
Mappings: []RouteMapping{
{
Method: "get",
Path: "/api/ping",
},
},
Http: &HttpClientConf{
Target: "localhost:45678",
Prefix: "\x7f/api",
},
},
}
go s.Start()
defer s.Stop()
time.Sleep(time.Millisecond * 200)
t.Run("/api/ping", func(t *testing.T) {
resp, err := httpc.Do(context.Background(), http.MethodGet,
"http://localhost:18883/api/ping", nil)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
func TestHttpToHttpBadWriter(t *testing.T) {
t.Run("bad url", func(t *testing.T) {
handler := new(Server).buildHttpHandler(&HttpClientConf{
Target: "http://example.com",
Timeout: 3000,
})
w := httptest.NewRecorder()
handler.ServeHTTP(&badResponseWriter{w},
httptest.NewRequest(http.MethodGet, "http://localhost:18884", nil))
assert.Equal(t, http.StatusBadRequest, w.Code)
})
t.Run("bad url", func(t *testing.T) {
var c GatewayConf
assert.NoError(t, conf.FillDefault(&c))
c.DevServer.Host = "localhost"
c.Host = "localhost"
c.Port = 18884
s := MustNewServer(c)
s.upstreams = []Upstream{
{
Mappings: []RouteMapping{
{
Method: "get",
Path: "/api/ping",
},
},
Http: &HttpClientConf{
Target: "localhost:45678",
Prefix: "\x7f/api",
},
},
}
go s.Start()
defer s.Stop()
handler := new(Server).buildHttpHandler(&HttpClientConf{
Target: "localhost:18884",
Timeout: 3000,
})
w := httptest.NewRecorder()
handler.ServeHTTP(&badResponseWriter{w},
httptest.NewRequest(http.MethodGet, "http://localhost:18884/api/ping", nil))
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
// Handler function for the root route
func pingHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("pong"))
}
func startTestServer(t *testing.T) *http.Server {
http.HandleFunc("/api/ping", pingHandler)
server := &http.Server{
Addr: ":45678",
Handler: http.DefaultServeMux,
}
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
t.Errorf("failed to start server: %v", err)
}
}()
return server
}
type badResponseWriter struct {
http.ResponseWriter
}
func (w *badResponseWriter) Write([]byte) (int, error) {
return 0, errors.New("bad writer")
}

63
go.mod
View File

@@ -1,28 +1,28 @@
module github.com/zeromicro/go-zero
go 1.20
go 1.21
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alicebob/miniredis/v2 v2.33.0
github.com/fatih/color v1.17.0
github.com/fullstorydev/grpcurl v1.9.1
github.com/go-sql-driver/mysql v1.8.1
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/alicebob/miniredis/v2 v2.35.0
github.com/fatih/color v1.18.0
github.com/fullstorydev/grpcurl v1.9.3
github.com/go-sql-driver/mysql v1.9.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.4
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.6.0
github.com/jhump/protoreflect v1.16.0
github.com/olekukonko/tablewriter v0.0.5
github.com/grafana/pyroscope-go v1.2.2
github.com/jackc/pgx/v5 v5.7.4
github.com/jhump/protoreflect v1.17.0
github.com/pelletier/go-toml/v2 v2.2.2
github.com/prometheus/client_golang v1.19.1
github.com/redis/go-redis/v9 v9.6.1
github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.10.0
github.com/spaolacci/murmur3 v1.1.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
go.etcd.io/etcd/api/v3 v3.5.15
go.etcd.io/etcd/client/v3 v3.5.15
go.mongodb.org/mongo-driver v1.16.0
go.mongodb.org/mongo-driver v1.17.4
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
@@ -31,14 +31,14 @@ require (
go.opentelemetry.io/otel/exporters/zipkin v1.24.0
go.opentelemetry.io/otel/sdk v1.24.0
go.opentelemetry.io/otel/trace v1.24.0
go.uber.org/automaxprocs v1.5.3
go.uber.org/automaxprocs v1.6.0
go.uber.org/goleak v1.3.0
golang.org/x/net v0.27.0
golang.org/x/sys v0.22.0
golang.org/x/time v0.5.0
golang.org/x/net v0.35.0
golang.org/x/sys v0.30.0
golang.org/x/time v0.10.0
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
google.golang.org/protobuf v1.36.5
gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0
@@ -50,9 +50,8 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.10.0 // indirect
github.com/bufbuild/protocompile v0.14.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
@@ -73,14 +72,16 @@ require (
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -91,15 +92,15 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
@@ -108,11 +109,11 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

136
go.sum
View File

@@ -2,17 +2,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM=
github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -35,10 +36,10 @@ github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjl
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fullstorydev/grpcurl v1.9.1 h1:YxX1aCcCc4SDBQfj9uoWcTLe8t4NWrZe1y+mk83BQgo=
github.com/fullstorydev/grpcurl v1.9.1/go.mod h1:i8gKLIC6s93WdU3LSmkE5vtsCxyRmihUj5FK1cNW5EM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk=
github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -52,14 +53,15 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -75,8 +77,13 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@@ -85,12 +92,12 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg=
github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -98,14 +105,17 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -113,7 +123,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -127,34 +136,38 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -166,16 +179,17 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@@ -188,8 +202,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
@@ -214,8 +228,8 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
@@ -224,11 +238,10 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -240,17 +253,17 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -262,20 +275,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -283,6 +296,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -293,8 +307,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -77,10 +77,12 @@ func (s *Server) StartAsync(c Config) {
// StartAgent start inner http server by config.
func StartAgent(c Config) {
if !c.Enabled {
return
}
once.Do(func() {
if c.Enabled {
s := NewServer(c)
s.StartAsync(c)
}
s := NewServer(c)
s.StartAsync(c)
})
}

View File

@@ -111,6 +111,10 @@ func (p *comboHealthManager) IsReady() bool {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.probes) == 0 {
return false
}
for _, probe := range p.probes {
if !probe.IsReady() {
return false

View File

@@ -43,7 +43,7 @@ func TestComboHealthManager(t *testing.T) {
hm1 := NewHealthManager(probeName)
hm2 := NewHealthManager(probeName + "2")
assert.True(t, chm.IsReady())
assert.False(t, chm.IsReady())
chm.addProbe(hm1)
chm.addProbe(hm2)
assert.False(t, chm.IsReady())
@@ -57,7 +57,7 @@ func TestComboHealthManager(t *testing.T) {
chm := newComboHealthManager()
hm := NewHealthManager(probeName)
assert.True(t, chm.IsReady())
assert.False(t, chm.IsReady())
chm.addProbe(hm)
assert.False(t, chm.IsReady())
hm.MarkReady()
@@ -127,7 +127,7 @@ func TestCreateHttpHandler(t *testing.T) {
resp, err := http.Get(srv.URL)
assert.Nil(t, err)
_ = resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
hm := NewHealthManager(probeName)
defaultHealthManager.addProbe(hm)

View File

@@ -0,0 +1,263 @@
package profiling
import (
"runtime"
"sync"
"time"
"github.com/grafana/pyroscope-go"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/threading"
)
const (
defaultCheckInterval = time.Second * 10
defaultProfilingDuration = time.Minute * 2
defaultUploadRate = time.Second * 15
)
type (
Config struct {
// Name is the name of the application.
Name string `json:",optional,inherit"`
// ServerAddr is the address of the profiling server.
ServerAddr string
// AuthUser is the username for basic authentication.
AuthUser string `json:",optional"`
// AuthPassword is the password for basic authentication.
AuthPassword string `json:",optional"`
// UploadRate is the duration for which profiling data is uploaded.
UploadRate time.Duration `json:",default=15s"`
// CheckInterval is the interval to check if profiling should start.
CheckInterval time.Duration `json:",default=10s"`
// ProfilingDuration is the duration for which profiling data is collected.
ProfilingDuration time.Duration `json:",default=2m"`
// CpuThreshold the collection is allowed only when the current service cpu < CpuThreshold
CpuThreshold int64 `json:",default=700,range=[0:1000)"`
// ProfileType is the type of profiling to be performed.
ProfileType ProfileType
}
ProfileType struct {
// Logger is a flag to enable or disable logging.
Logger bool `json:",default=false"`
// CPU is a flag to disable CPU profiling.
CPU bool `json:",default=true"`
// Goroutines is a flag to disable goroutine profiling.
Goroutines bool `json:",default=true"`
// Memory is a flag to disable memory profiling.
Memory bool `json:",default=true"`
// Mutex is a flag to disable mutex profiling.
Mutex bool `json:",default=false"`
// Block is a flag to disable block profiling.
Block bool `json:",default=false"`
}
profiler interface {
Start() error
Stop() error
}
pyroscopeProfiler struct {
c Config
profiler *pyroscope.Profiler
}
)
var (
once sync.Once
newProfiler = func(c Config) profiler {
return newPyroscopeProfiler(c)
}
)
// Start initializes the pyroscope profiler with the given configuration.
func Start(c Config) {
// check if the profiling is enabled
if len(c.ServerAddr) == 0 {
return
}
// set default values for the configuration
if c.ProfilingDuration <= 0 {
c.ProfilingDuration = defaultProfilingDuration
}
// set default values for the configuration
if c.CheckInterval <= 0 {
c.CheckInterval = defaultCheckInterval
}
if c.UploadRate <= 0 {
c.UploadRate = defaultUploadRate
}
once.Do(func() {
logx.Info("continuous profiling started")
threading.GoSafe(func() {
startPyroscope(c, proc.Done())
})
})
}
// startPyroscope starts the pyroscope profiler with the given configuration.
func startPyroscope(c Config, done <-chan struct{}) {
var (
pr profiler
err error
latestProfilingTime time.Time
intervalTicker = time.NewTicker(c.CheckInterval)
profilingTicker = time.NewTicker(c.ProfilingDuration)
)
defer profilingTicker.Stop()
defer intervalTicker.Stop()
for {
select {
case <-intervalTicker.C:
// Check if the machine is overloaded and if the profiler is not running
if pr == nil && isCpuOverloaded(c) {
pr = newProfiler(c)
if err := pr.Start(); err != nil {
logx.Errorf("failed to start profiler: %v", err)
continue
}
// record the latest profiling time
latestProfilingTime = time.Now()
logx.Infof("pyroscope profiler started.")
}
case <-profilingTicker.C:
// check if the profiling duration has passed
if !time.Now().After(latestProfilingTime.Add(c.ProfilingDuration)) {
continue
}
// check if the profiler is already running, if so, skip
if pr != nil {
if err = pr.Stop(); err != nil {
logx.Errorf("failed to stop profiler: %v", err)
}
logx.Infof("pyroscope profiler stopped.")
pr = nil
}
case <-done:
logx.Infof("continuous profiling stopped.")
return
}
}
}
// genPyroscopeConf generates the pyroscope configuration based on the given config.
func genPyroscopeConf(c Config) pyroscope.Config {
pConf := pyroscope.Config{
UploadRate: c.UploadRate,
ApplicationName: c.Name,
BasicAuthUser: c.AuthUser, // http basic auth user
BasicAuthPassword: c.AuthPassword, // http basic auth password
ServerAddress: c.ServerAddr,
Logger: nil,
HTTPHeaders: map[string]string{},
// you can provide static tags via a map:
Tags: map[string]string{
"name": c.Name,
},
}
if c.ProfileType.Logger {
pConf.Logger = logx.WithCallerSkip(0)
}
if c.ProfileType.CPU {
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileCPU)
}
if c.ProfileType.Goroutines {
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileGoroutines)
}
if c.ProfileType.Memory {
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileAllocObjects, pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects, pyroscope.ProfileInuseSpace)
}
if c.ProfileType.Mutex {
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileMutexCount, pyroscope.ProfileMutexDuration)
}
if c.ProfileType.Block {
pConf.ProfileTypes = append(pConf.ProfileTypes, pyroscope.ProfileBlockCount, pyroscope.ProfileBlockDuration)
}
logx.Infof("applicationName: %s", pConf.ApplicationName)
return pConf
}
// isCpuOverloaded checks the machine performance based on the given configuration.
func isCpuOverloaded(c Config) bool {
currentValue := stat.CpuUsage()
if currentValue >= c.CpuThreshold {
logx.Infof("continuous profiling cpu overload, cpu: %d", currentValue)
return true
}
return false
}
func newPyroscopeProfiler(c Config) profiler {
return &pyroscopeProfiler{
c: c,
}
}
func (p *pyroscopeProfiler) Start() error {
pConf := genPyroscopeConf(p.c)
// set mutex and block profile rate
setFraction(p.c)
prof, err := pyroscope.Start(pConf)
if err != nil {
resetFraction(p.c)
return err
}
p.profiler = prof
return nil
}
func (p *pyroscopeProfiler) Stop() error {
if p.profiler == nil {
return nil
}
if err := p.profiler.Stop(); err != nil {
return err
}
resetFraction(p.c)
p.profiler = nil
return nil
}
func setFraction(c Config) {
// These 2 lines are only required if you're using mutex or block profiling
if c.ProfileType.Mutex {
runtime.SetMutexProfileFraction(10) // 10/seconds
}
if c.ProfileType.Block {
runtime.SetBlockProfileRate(1000 * 1000) // 1/millisecond
}
}
func resetFraction(c Config) {
// These 2 lines are only required if you're using mutex or block profiling
if c.ProfileType.Mutex {
runtime.SetMutexProfileFraction(0)
}
if c.ProfileType.Block {
runtime.SetBlockProfileRate(0)
}
}

View File

@@ -0,0 +1,177 @@
package profiling
import (
"sync"
"testing"
"time"
"github.com/grafana/pyroscope-go"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/syncx"
)
func TestStart(t *testing.T) {
t.Run("profiling", func(t *testing.T) {
var c Config
assert.NoError(t, conf.FillDefault(&c))
c.Name = "test"
p := newProfiler(c)
assert.NotNil(t, p)
assert.NoError(t, p.Start())
assert.NoError(t, p.Stop())
})
t.Run("invalid config", func(t *testing.T) {
mp := &mockProfiler{}
newProfiler = func(c Config) profiler {
return mp
}
Start(Config{})
Start(Config{
ServerAddr: "localhost:4040",
})
})
t.Run("test start profiler", func(t *testing.T) {
mp := &mockProfiler{}
newProfiler = func(c Config) profiler {
return mp
}
c := Config{
Name: "test",
ServerAddr: "localhost:4040",
CheckInterval: time.Millisecond,
ProfilingDuration: time.Millisecond * 10,
CpuThreshold: 0,
}
var done = make(chan struct{})
go startPyroscope(c, done)
time.Sleep(time.Millisecond * 50)
close(done)
assert.True(t, mp.started.True())
assert.True(t, mp.stopped.True())
})
t.Run("test start profiler with cpu overloaded", func(t *testing.T) {
mp := &mockProfiler{}
newProfiler = func(c Config) profiler {
return mp
}
c := Config{
Name: "test",
ServerAddr: "localhost:4040",
CheckInterval: time.Millisecond,
ProfilingDuration: time.Millisecond * 10,
CpuThreshold: 900,
}
var done = make(chan struct{})
go startPyroscope(c, done)
time.Sleep(time.Millisecond * 50)
close(done)
assert.False(t, mp.started.True())
})
t.Run("start/stop err", func(t *testing.T) {
mp := &mockProfiler{
err: assert.AnError,
}
newProfiler = func(c Config) profiler {
return mp
}
c := Config{
Name: "test",
ServerAddr: "localhost:4040",
CheckInterval: time.Millisecond,
ProfilingDuration: time.Millisecond * 10,
CpuThreshold: 0,
}
var done = make(chan struct{})
go startPyroscope(c, done)
time.Sleep(time.Millisecond * 50)
close(done)
assert.False(t, mp.started.True())
assert.False(t, mp.stopped.True())
})
}
func TestGenPyroscopeConf(t *testing.T) {
c := Config{
Name: "",
ServerAddr: "localhost:4040",
AuthUser: "user",
AuthPassword: "password",
ProfileType: ProfileType{
Logger: true,
CPU: true,
Goroutines: true,
Memory: true,
Mutex: true,
Block: true,
},
}
pyroscopeConf := genPyroscopeConf(c)
assert.Equal(t, c.ServerAddr, pyroscopeConf.ServerAddress)
assert.Equal(t, c.AuthUser, pyroscopeConf.BasicAuthUser)
assert.Equal(t, c.AuthPassword, pyroscopeConf.BasicAuthPassword)
assert.Equal(t, c.Name, pyroscopeConf.ApplicationName)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileCPU)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileGoroutines)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileAllocObjects)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileAllocSpace)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileInuseObjects)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileInuseSpace)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileMutexCount)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileMutexDuration)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileBlockCount)
assert.Contains(t, pyroscopeConf.ProfileTypes, pyroscope.ProfileBlockDuration)
setFraction(c)
resetFraction(c)
newPyroscopeProfiler(c)
}
func TestNewPyroscopeProfiler(t *testing.T) {
p := newPyroscopeProfiler(Config{})
assert.Error(t, p.Start())
assert.NoError(t, p.Stop())
}
type mockProfiler struct {
mutex sync.Mutex
started syncx.AtomicBool
stopped syncx.AtomicBool
err error
}
func (m *mockProfiler) Start() error {
m.mutex.Lock()
if m.err == nil {
m.started.Set(true)
}
m.mutex.Unlock()
return m.err
}
func (m *mockProfiler) Stop() error {
m.mutex.Lock()
if m.err == nil {
m.stopped.Set(true)
}
m.mutex.Unlock()
return m.err
}

43
mcp/config.go Normal file
View File

@@ -0,0 +1,43 @@
package mcp
import (
"time"
"github.com/zeromicro/go-zero/rest"
)
// McpConf defines the configuration for an MCP server.
// It embeds rest.RestConf for HTTP server settings
// and adds MCP-specific configuration options.
type McpConf struct {
rest.RestConf
Mcp struct {
// Name is the server name reported in initialize responses
Name string `json:",optional"`
// Version is the server version reported in initialize responses
Version string `json:",default=1.0.0"`
// ProtocolVersion is the MCP protocol version implemented
ProtocolVersion string `json:",default=2024-11-05"`
// BaseUrl is the base URL for the server, used in SSE endpoint messages
// If not set, defaults to http://localhost:{Port}
BaseUrl string `json:",optional"`
// SseEndpoint is the path for Server-Sent Events connections
SseEndpoint string `json:",default=/sse"`
// MessageEndpoint is the path for JSON-RPC requests
MessageEndpoint string `json:",default=/message"`
// Cors contains allowed CORS origins
Cors []string `json:",optional"`
// SseTimeout is the maximum time allowed for SSE connections
SseTimeout time.Duration `json:",default=24h"`
// MessageTimeout is the maximum time allowed for request execution
MessageTimeout time.Duration `json:",default=30s"`
}
}

63
mcp/config_test.go Normal file
View File

@@ -0,0 +1,63 @@
package mcp
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/conf"
)
func TestMcpConfDefaults(t *testing.T) {
// Test default values are set correctly when unmarshalled from JSON
jsonConfig := `name: test-service
port: 8080
mcp:
name: test-mcp-server
version: 1.0.0
`
var c McpConf
err := conf.LoadFromYamlBytes([]byte(jsonConfig), &c)
assert.NoError(t, err)
// Check default values
assert.Equal(t, "test-mcp-server", c.Mcp.Name)
assert.Equal(t, "1.0.0", c.Mcp.Version, "Default version should be 1.0.0")
assert.Equal(t, "2024-11-05", c.Mcp.ProtocolVersion, "Default protocol version should be 2024-11-05")
assert.Equal(t, "/sse", c.Mcp.SseEndpoint, "Default SSE endpoint should be /sse")
assert.Equal(t, "/message", c.Mcp.MessageEndpoint, "Default message endpoint should be /message")
assert.Equal(t, 30*time.Second, c.Mcp.MessageTimeout, "Default message timeout should be 30s")
}
func TestMcpConfCustomValues(t *testing.T) {
// Test custom values can be set
jsonConfig := `{
"Name": "test-service",
"Port": 8080,
"Mcp": {
"Name": "test-mcp-server",
"Version": "2.0.0",
"ProtocolVersion": "2025-01-01",
"BaseUrl": "http://example.com",
"SseEndpoint": "/custom-sse",
"MessageEndpoint": "/custom-message",
"Cors": ["http://localhost:3000", "http://example.com"],
"MessageTimeout": "60s"
}
}`
var c McpConf
err := conf.LoadFromJsonBytes([]byte(jsonConfig), &c)
assert.NoError(t, err)
// Check custom values
assert.Equal(t, "test-mcp-server", c.Mcp.Name, "Name should be inherited from RestConf")
assert.Equal(t, "2.0.0", c.Mcp.Version, "Version should be customizable")
assert.Equal(t, "2025-01-01", c.Mcp.ProtocolVersion, "Protocol version should be customizable")
assert.Equal(t, "http://example.com", c.Mcp.BaseUrl, "BaseUrl should be customizable")
assert.Equal(t, "/custom-sse", c.Mcp.SseEndpoint, "SSE endpoint should be customizable")
assert.Equal(t, "/custom-message", c.Mcp.MessageEndpoint, "Message endpoint should be customizable")
assert.Equal(t, []string{"http://localhost:3000", "http://example.com"}, c.Mcp.Cors, "CORS settings should be customizable")
assert.Equal(t, 60*time.Second, c.Mcp.MessageTimeout, "Tool timeout should be customizable")
}

443
mcp/integration_test.go Normal file
View File

@@ -0,0 +1,443 @@
package mcp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// syncResponseRecorder is a thread-safe wrapper around httptest.ResponseRecorder
type syncResponseRecorder struct {
*httptest.ResponseRecorder
mu sync.Mutex
}
// Create a new synchronized response recorder
func newSyncResponseRecorder() *syncResponseRecorder {
return &syncResponseRecorder{
ResponseRecorder: httptest.NewRecorder(),
}
}
// Override Write method to synchronize access
func (srr *syncResponseRecorder) Write(p []byte) (int, error) {
srr.mu.Lock()
defer srr.mu.Unlock()
return srr.ResponseRecorder.Write(p)
}
// Override WriteHeader method to synchronize access
func (srr *syncResponseRecorder) WriteHeader(statusCode int) {
srr.mu.Lock()
defer srr.mu.Unlock()
srr.ResponseRecorder.WriteHeader(statusCode)
}
// Override Result method to synchronize access
func (srr *syncResponseRecorder) Result() *http.Response {
srr.mu.Lock()
defer srr.mu.Unlock()
return srr.ResponseRecorder.Result()
}
// TestHTTPHandlerIntegration tests the HTTP handlers with a real server instance
func TestHTTPHandlerIntegration(t *testing.T) {
// Skip in short test mode
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Create a test configuration
conf := McpConf{}
conf.Mcp.Name = "test-integration"
conf.Mcp.Version = "1.0.0-test"
conf.Mcp.MessageTimeout = 1 * time.Second
// Create a mock server directly
server := &sseMcpServer{
conf: conf,
clients: make(map[string]*mcpClient),
tools: make(map[string]Tool),
prompts: make(map[string]Prompt),
resources: make(map[string]Resource),
}
// Register a test tool
err := server.RegisterTool(Tool{
Name: "echo",
Description: "Echo tool for testing",
InputSchema: InputSchema{
Properties: map[string]any{
"message": map[string]any{
"type": "string",
"description": "Message to echo",
},
},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
if msg, ok := params["message"].(string); ok {
return fmt.Sprintf("Echo: %s", msg), nil
}
return "Echo: no message provided", nil
},
})
require.NoError(t, err)
// Create a test HTTP request to the SSE endpoint
req := httptest.NewRequest("GET", "/sse", nil)
w := newSyncResponseRecorder()
// Create a done channel to signal completion of test
done := make(chan bool)
// Start the SSE handler in a goroutine
go func() {
// lock.Lock()
server.handleSSE(w, req)
// lock.Unlock()
done <- true
}()
// Allow time for the handler to process
select {
case <-time.After(100 * time.Millisecond):
// Expected - handler would normally block indefinitely
case <-done:
// This shouldn't happen immediately - the handler should block
t.Error("SSE handler returned unexpectedly")
}
// Check the initial headers
resp := w.Result()
assert.Equal(t, "chunked", resp.Header.Get("Transfer-Encoding"))
resp.Body.Close()
// The handler creates a client and sends the endpoint message
var sessionId string
// Give the handler time to set up the client
time.Sleep(50 * time.Millisecond)
// Check that a client was created
server.clientsLock.Lock()
assert.Equal(t, 1, len(server.clients))
for id := range server.clients {
sessionId = id
}
server.clientsLock.Unlock()
require.NotEmpty(t, sessionId, "Expected a session ID to be created")
// Now that we have a session ID, we can test the message endpoint
messageBody, _ := json.Marshal(Request{
JsonRpc: "2.0",
ID: 1,
Method: methodInitialize,
Params: json.RawMessage(`{}`),
})
// Create a message request
reqURL := fmt.Sprintf("/message?%s=%s", sessionIdKey, sessionId)
msgReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(messageBody))
msgW := newSyncResponseRecorder()
// Process the message
server.handleRequest(msgW, msgReq)
// Check the response
msgResp := msgW.Result()
assert.Equal(t, http.StatusAccepted, msgResp.StatusCode)
msgResp.Body.Close() // Ensure response body is closed
}
// TestHandlerResponseFlow tests the flow of a full request/response cycle
func TestHandlerResponseFlow(t *testing.T) {
// Create a mock server for testing
server := &sseMcpServer{
conf: McpConf{},
clients: map[string]*mcpClient{
"test-session": {
id: "test-session",
channel: make(chan string, 10),
initialized: true,
},
},
tools: make(map[string]Tool),
prompts: make(map[string]Prompt),
resources: make(map[string]Resource),
}
// Register test resources
server.RegisterTool(Tool{
Name: "test.tool",
Description: "Test tool",
InputSchema: InputSchema{Type: "object"},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
return "tool result", nil
},
})
server.RegisterPrompt(Prompt{
Name: "test.prompt",
Description: "Test prompt",
})
server.RegisterResource(Resource{
Name: "test.resource",
URI: "http://example.com",
Description: "Test resource",
})
// Create a request with session ID parameter
reqURL := fmt.Sprintf("/message?%s=%s", sessionIdKey, "test-session")
// Test tools/list request
toolsListBody, _ := json.Marshal(Request{
JsonRpc: "2.0",
ID: 1,
Method: methodToolsList,
Params: json.RawMessage(`{}`),
})
toolsReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(toolsListBody))
toolsW := newSyncResponseRecorder()
// Process the request
server.handleRequest(toolsW, toolsReq)
// Check the response code
toolsResp := toolsW.Result()
assert.Equal(t, http.StatusAccepted, toolsResp.StatusCode)
toolsResp.Body.Close()
// Check the channel message
client := server.clients["test-session"]
select {
case message := <-client.channel:
assert.Contains(t, message, `"tools":[{"name":"test.tool"`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for tools/list response")
}
// Test prompts/list request
promptsListBody, _ := json.Marshal(Request{
JsonRpc: "2.0",
ID: 2,
Method: methodPromptsList,
Params: json.RawMessage(`{}`),
})
promptsReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(promptsListBody))
promptsW := newSyncResponseRecorder()
// Process the request
server.handleRequest(promptsW, promptsReq)
// Check the response code
promptsResp := promptsW.Result()
assert.Equal(t, http.StatusAccepted, promptsResp.StatusCode)
promptsResp.Body.Close()
// Check the channel message
select {
case message := <-client.channel:
assert.Contains(t, message, `"prompts":[{"name":"test.prompt"`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for prompts/list response")
}
// Test resources/list request
resourcesListBody, _ := json.Marshal(Request{
JsonRpc: "2.0",
ID: 3,
Method: methodResourcesList,
Params: json.RawMessage(`{}`),
})
resourcesReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(resourcesListBody))
resourcesW := newSyncResponseRecorder()
// Process the request
server.handleRequest(resourcesW, resourcesReq)
// Check the response code
resourcesResp := resourcesW.Result()
assert.Equal(t, http.StatusAccepted, resourcesResp.StatusCode)
resourcesResp.Body.Close()
// Check the channel message
select {
case message := <-client.channel:
assert.Contains(t, message, `"name":"test.resource"`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for resources/list response")
}
}
// TestProcessListMethods tests the list processing methods with pagination
func TestProcessListMethods(t *testing.T) {
server := &sseMcpServer{
tools: make(map[string]Tool),
prompts: make(map[string]Prompt),
resources: make(map[string]Resource),
}
// Add some test data
for i := 1; i <= 5; i++ {
tool := Tool{
Name: fmt.Sprintf("tool%d", i),
Description: fmt.Sprintf("Tool %d", i),
InputSchema: InputSchema{Type: "object"},
}
server.tools[tool.Name] = tool
prompt := Prompt{
Name: fmt.Sprintf("prompt%d", i),
Description: fmt.Sprintf("Prompt %d", i),
}
server.prompts[prompt.Name] = prompt
resource := Resource{
Name: fmt.Sprintf("resource%d", i),
URI: fmt.Sprintf("http://example.com/%d", i),
Description: fmt.Sprintf("Resource %d", i),
}
server.resources[resource.Name] = resource
}
// Create a test client
client := &mcpClient{
id: "test-client",
channel: make(chan string, 10),
initialized: true,
}
// Test processListTools
req := Request{
JsonRpc: "2.0",
ID: 1,
Method: methodToolsList,
Params: json.RawMessage(`{"cursor": "", "_meta": {"progressToken": "token1"}}`),
}
server.processListTools(context.Background(), client, req)
// Read response
select {
case response := <-client.channel:
assert.Contains(t, response, `"tools":`)
assert.Contains(t, response, `"progressToken":"token1"`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for tools/list response")
}
// Test processListPrompts
req.ID = 2
req.Method = methodPromptsList
req.Params = json.RawMessage(`{"cursor": "next"}`)
server.processListPrompts(context.Background(), client, req)
// Read response
select {
case response := <-client.channel:
assert.Contains(t, response, `"prompts":`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for prompts/list response")
}
// Test processListResources
req.ID = 3
req.Method = methodResourcesList
req.Params = json.RawMessage(`{"cursor": "next"}`)
server.processListResources(context.Background(), client, req)
// Read response
select {
case response := <-client.channel:
assert.Contains(t, response, `"resources":`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for resources/list response")
}
}
// TestErrorResponseHandling tests error handling in the server
func TestErrorResponseHandling(t *testing.T) {
server := &sseMcpServer{
tools: make(map[string]Tool),
prompts: make(map[string]Prompt),
resources: make(map[string]Resource),
}
// Create a test client
client := &mcpClient{
id: "test-client",
channel: make(chan string, 10),
initialized: true,
}
// Test invalid method
req := Request{
JsonRpc: "2.0",
ID: 1,
Method: "invalid_method",
Params: json.RawMessage(`{}`),
}
// Mock handleRequest by directly calling error handler
server.sendErrorResponse(context.Background(), client, req.ID, "Method not found", errCodeMethodNotFound)
// Check response
select {
case response := <-client.channel:
assert.Contains(t, response, `"error":{"code":-32601,"message":"Method not found"}`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for error response")
}
// Test invalid tool
toolReq := Request{
JsonRpc: "2.0",
ID: 2,
Method: methodToolsCall,
Params: json.RawMessage(`{"name":"non_existent_tool"}`),
}
// Call process method directly
server.processToolCall(context.Background(), client, toolReq)
// Check response
select {
case response := <-client.channel:
assert.Contains(t, response, `"error":{"code":-32602,"message":"Tool 'non_existent_tool' not found"}`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for error response")
}
// Test invalid prompt
promptReq := Request{
JsonRpc: "2.0",
ID: 3,
Method: methodPromptsGet,
Params: json.RawMessage(`{"name":"non_existent_prompt"}`),
}
// Call process method directly
server.processGetPrompt(context.Background(), client, promptReq)
// Check response
select {
case response := <-client.channel:
assert.Contains(t, response, `"error":{"code":-32602,"message":"Prompt 'non_existent_prompt' not found"}`)
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for error response")
}
}

23
mcp/parser.go Normal file
View File

@@ -0,0 +1,23 @@
package mcp
import (
"fmt"
"github.com/zeromicro/go-zero/core/mapping"
)
// ParseArguments parses the arguments and populates the request object
func ParseArguments(args any, req any) error {
switch arguments := args.(type) {
case map[string]string:
m := make(map[string]any, len(arguments))
for k, v := range arguments {
m[k] = v
}
return mapping.UnmarshalJsonMap(m, req, mapping.WithStringValues())
case map[string]any:
return mapping.UnmarshalJsonMap(arguments, req)
default:
return fmt.Errorf("unsupported argument type: %T", arguments)
}
}

139
mcp/parser_test.go Normal file
View File

@@ -0,0 +1,139 @@
package mcp
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestParseArguments_MapStringString tests parsing map[string]string arguments
func TestParseArguments_MapStringString(t *testing.T) {
// Sample request struct to populate
type requestStruct struct {
Name string `json:"name"`
Message string `json:"message"`
Count int `json:"count"`
Enabled bool `json:"enabled"`
}
// Create test arguments
args := map[string]string{
"name": "test-name",
"message": "hello world",
"count": "42",
"enabled": "true",
}
// Create a target object to populate
var req requestStruct
// Parse the arguments
err := ParseArguments(args, &req)
// Verify results
assert.NoError(t, err, "Should parse map[string]string without error")
assert.Equal(t, "test-name", req.Name, "Name should be correctly parsed")
assert.Equal(t, "hello world", req.Message, "Message should be correctly parsed")
assert.Equal(t, 42, req.Count, "Count should be correctly parsed to int")
assert.True(t, req.Enabled, "Enabled should be correctly parsed to bool")
}
// TestParseArguments_MapStringAny tests parsing map[string]any arguments
func TestParseArguments_MapStringAny(t *testing.T) {
// Sample request struct to populate
type requestStruct struct {
Name string `json:"name"`
Message string `json:"message"`
Count int `json:"count"`
Enabled bool `json:"enabled"`
Tags []string `json:"tags"`
Metadata map[string]string `json:"metadata"`
}
// Create test arguments with mixed types
args := map[string]any{
"name": "test-name",
"message": "hello world",
"count": 42, // note: this is already an int
"enabled": true, // note: this is already a bool
"tags": []string{"tag1", "tag2"},
"metadata": map[string]string{
"key1": "value1",
"key2": "value2",
},
}
// Create a target object to populate
var req requestStruct
// Parse the arguments
err := ParseArguments(args, &req)
// Verify results
assert.NoError(t, err, "Should parse map[string]any without error")
assert.Equal(t, "test-name", req.Name, "Name should be correctly parsed")
assert.Equal(t, "hello world", req.Message, "Message should be correctly parsed")
assert.Equal(t, 42, req.Count, "Count should be correctly parsed")
assert.True(t, req.Enabled, "Enabled should be correctly parsed")
assert.Equal(t, []string{"tag1", "tag2"}, req.Tags, "Tags should be correctly parsed")
assert.Equal(t, map[string]string{
"key1": "value1",
"key2": "value2",
}, req.Metadata, "Metadata should be correctly parsed")
}
// TestParseArguments_UnsupportedType tests parsing with an unsupported type
func TestParseArguments_UnsupportedType(t *testing.T) {
// Sample request struct to populate
type requestStruct struct {
Name string `json:"name"`
Message string `json:"message"`
}
// Use an unsupported argument type (slice)
args := []string{"not", "a", "map"}
// Create a target object to populate
var req requestStruct
// Parse the arguments
err := ParseArguments(args, &req)
// Verify error is returned with correct message
assert.Error(t, err, "Should return error for unsupported type")
assert.Contains(t, err.Error(), "unsupported argument type", "Error should mention unsupported type")
assert.Contains(t, err.Error(), "[]string", "Error should include the actual type")
}
// TestParseArguments_EmptyMap tests parsing with empty maps
func TestParseArguments_EmptyMap(t *testing.T) {
// Sample request struct to populate
type requestStruct struct {
Name string `json:"name,optional"`
Message string `json:"message,optional"`
}
// Test empty map[string]string
t.Run("EmptyMapStringString", func(t *testing.T) {
args := map[string]string{}
var req requestStruct
err := ParseArguments(args, &req)
assert.NoError(t, err, "Should parse empty map[string]string without error")
assert.Empty(t, req.Name, "Name should be empty string")
assert.Empty(t, req.Message, "Message should be empty string")
})
// Test empty map[string]any
t.Run("EmptyMapStringAny", func(t *testing.T) {
args := map[string]any{}
var req requestStruct
err := ParseArguments(args, &req)
assert.NoError(t, err, "Should parse empty map[string]any without error")
assert.Empty(t, req.Name, "Name should be empty string")
assert.Empty(t, req.Message, "Message should be empty string")
})
}

870
mcp/readme.md Normal file
View File

@@ -0,0 +1,870 @@
# Model Context Protocol (MCP) Implementation
## Overview
This package implements the Model Context Protocol (MCP) server specification in Go, providing a framework for real-time communication between AI models and clients using Server-Sent Events (SSE). The implementation follows the standardized protocol for building AI-assisted applications with bidirectional communication capabilities.
## Core Components
### Server-Sent Events (SSE) Communication
- **Real-time Communication**: Robust SSE-based communication system that maintains persistent connections with clients
- **Connection Management**: Client registration, message broadcasting, and client cleanup mechanisms
- **Event Handling**: Event types for tools, prompts, and resources changes
### JSON-RPC Implementation
- **Request Processing**: Complete JSON-RPC request processor for handling MCP protocol methods
- **Response Formatting**: Proper response formatting according to JSON-RPC specifications
- **Error Handling**: Comprehensive error handling with appropriate error codes
### Tool Management
- **Tool Registration**: System to register custom tools with handlers
- **Tool Execution**: Mechanism to execute tool functions with proper timeout handling
- **Result Handling**: Flexible result handling supporting various return types (string, JSON, images)
### Prompt System
- **Prompt Registration**: System for registering both static and dynamic prompts
- **Argument Validation**: Validation for required arguments and default values for optional ones
- **Message Generation**: Handlers that generate properly formatted conversation messages
### Resource Management
- **Resource Registration**: System for managing and accessing external resources
- **Content Delivery**: Handlers for delivering resource content to clients on demand
- **Resource Subscription**: Mechanisms for clients to subscribe to resource updates
### Protocol Features
- **Initialization Sequence**: Proper handshaking with capability negotiation
- **Notification Handling**: Support for both standard and client-specific notifications
- **Message Routing**: Intelligent routing of requests to appropriate handlers
## Technical Highlights
### Configuration System
- **Flexible Configuration**: Configuration system with sensible defaults and customization options
- **CORS Support**: Configurable CORS settings for cross-origin requests
- **Server Information**: Proper server identification and versioning
### Client Session Management
- **Session Tracking**: Client session tracking with unique identifiers
- **Connection Health**: Ping/pong mechanism to maintain connection health
- **Initialization State**: Client initialization state tracking
### Content Handling
- **Multi-format Content**: Support for text, code, and binary content
- **MIME Type Support**: Proper MIME type identification for various content types
- **Audience Annotations**: Content audience annotations for user/assistant targeting
## Usage
### Setting Up an MCP Server
To create and start an MCP server:
```go
package main
import (
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/mcp"
)
func main() {
// Load configuration from YAML file
var c mcp.McpConf
conf.MustLoad("config.yaml", &c)
// Optional: Disable stats logging
logx.DisableStat()
// Create MCP server
server := mcp.NewMcpServer(c)
// Register tools, prompts, and resources (examples below)
// Start the server and ensure it's stopped on exit
defer server.Stop()
server.Start()
}
```
Sample configuration file (config.yaml):
```yaml
name: mcp-server
host: localhost
port: 8080
mcp:
name: my-mcp-server
messageTimeout: 30s # Timeout for tool calls
cors:
- http://localhost:3000 # Optional CORS configuration
```
### Registering Tools
Tools allow AI models to execute custom code through the MCP protocol.
#### Basic Tool Example:
```go
// Register a simple echo tool
echoTool := mcp.Tool{
Name: "echo",
Description: "Echoes back the message provided by the user",
InputSchema: mcp.InputSchema{
Properties: map[string]any{
"message": map[string]any{
"type": "string",
"description": "The message to echo back",
},
"prefix": map[string]any{
"type": "string",
"description": "Optional prefix to add to the echoed message",
"default": "Echo: ",
},
},
Required: []string{"message"},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
var req struct {
Message string `json:"message"`
Prefix string `json:"prefix,optional"`
}
if err := mcp.ParseArguments(params, &req); err != nil {
return nil, fmt.Errorf("failed to parse params: %w", err)
}
prefix := "Echo: "
if len(req.Prefix) > 0 {
prefix = req.Prefix
}
return prefix + req.Message, nil
},
}
server.RegisterTool(echoTool)
```
#### Tool with Different Response Types:
```go
// Tool returning JSON data
dataTool := mcp.Tool{
Name: "data.generate",
Description: "Generates sample data in various formats",
InputSchema: mcp.InputSchema{
Properties: map[string]any{
"format": map[string]any{
"type": "string",
"description": "Format of data (json, text)",
"enum": []string{"json", "text"},
},
},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
var req struct {
Format string `json:"format"`
}
if err := mcp.ParseArguments(params, &req); err != nil {
return nil, fmt.Errorf("failed to parse params: %w", err)
}
if req.Format == "json" {
// Return structured data
return map[string]any{
"items": []map[string]any{
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
},
"count": 2,
}, nil
}
// Default to text
return "Sample text data", nil
},
}
server.RegisterTool(dataTool)
```
#### Image Generation Tool Example:
```go
// Tool returning image content
imageTool := mcp.Tool{
Name: "image.generate",
Description: "Generates a simple image",
InputSchema: mcp.InputSchema{
Properties: map[string]any{
"type": map[string]any{
"type": "string",
"description": "Type of image to generate",
"default": "placeholder",
},
},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
// Return image content directly
return mcp.ImageContent{
Data: "base64EncodedImageData...", // Base64 encoded image data
MimeType: "image/png",
}, nil
},
}
server.RegisterTool(imageTool)
```
#### Using ToolResult for Custom Outputs:
```go
// Tool that returns a custom ToolResult type
customResultTool := mcp.Tool{
Name: "custom.result",
Description: "Returns a custom formatted result",
InputSchema: mcp.InputSchema{
Properties: map[string]any{
"resultType": map[string]any{
"type": "string",
"enum": []string{"text", "image"},
},
},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
var req struct {
ResultType string `json:"resultType"`
}
if err := mcp.ParseArguments(params, &req); err != nil {
return nil, fmt.Errorf("failed to parse params: %w", err)
}
if req.ResultType == "image" {
return mcp.ToolResult{
Type: mcp.ContentTypeImage,
Content: map[string]any{
"data": "base64EncodedImageData...",
"mimeType": "image/jpeg",
},
}, nil
}
// Default to text
return mcp.ToolResult{
Type: mcp.ContentTypeText,
Content: "This is a text result from ToolResult",
}, nil
},
}
server.RegisterTool(customResultTool)
```
### Registering Prompts
Prompts are reusable conversation templates for AI models.
#### Static Prompt Example:
```go
// Register a simple static prompt with placeholders
server.RegisterPrompt(mcp.Prompt{
Name: "hello",
Description: "A simple hello prompt",
Arguments: []mcp.PromptArgument{
{
Name: "name",
Description: "The name to greet",
Required: false,
},
},
Content: "Say hello to {{name}} and introduce yourself as an AI assistant.",
})
```
#### Dynamic Prompt with Handler Function:
```go
// Register a prompt with a dynamic handler function
server.RegisterPrompt(mcp.Prompt{
Name: "dynamic-prompt",
Description: "A prompt that uses a handler to generate dynamic content",
Arguments: []mcp.PromptArgument{
{
Name: "username",
Description: "User's name for personalized greeting",
Required: true,
},
{
Name: "topic",
Description: "Topic of expertise",
Required: true,
},
},
Handler: func(ctx context.Context, args map[string]string) ([]mcp.PromptMessage, error) {
var req struct {
Username string `json:"username"`
Topic string `json:"topic"`
}
if err := mcp.ParseArguments(args, &req); err != nil {
return nil, fmt.Errorf("failed to parse args: %w", err)
}
// Create a user message
userMessage := mcp.PromptMessage{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Text: fmt.Sprintf("Hello, I'm %s and I'd like to learn about %s.", req.Username, req.Topic),
},
}
// Create an assistant response with current time
currentTime := time.Now().Format(time.RFC1123)
assistantMessage := mcp.PromptMessage{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: fmt.Sprintf("Hello %s! I'm an AI assistant and I'll help you learn about %s. The current time is %s.",
req.Username, req.Topic, currentTime),
},
}
// Return both messages as a conversation
return []mcp.PromptMessage{userMessage, assistantMessage}, nil
},
})
```
#### Multi-Message Prompt with Code Examples:
```go
// Register a prompt that provides code examples in different programming languages
server.RegisterPrompt(mcp.Prompt{
Name: "code-example",
Description: "Provides code examples in different programming languages",
Arguments: []mcp.PromptArgument{
{
Name: "language",
Description: "Programming language for the example",
Required: true,
},
{
Name: "complexity",
Description: "Complexity level (simple, medium, advanced)",
},
},
Handler: func(ctx context.Context, args map[string]string) ([]mcp.PromptMessage, error) {
var req struct {
Language string `json:"language"`
Complexity string `json:"complexity,optional"`
}
if err := mcp.ParseArguments(args, &req); err != nil {
return nil, fmt.Errorf("failed to parse args: %w", err)
}
// Validate language
supportedLanguages := map[string]bool{"go": true, "python": true, "javascript": true, "rust": true}
if !supportedLanguages[req.Language] {
return nil, fmt.Errorf("unsupported language: %s", req.Language)
}
// Generate code example based on language and complexity
var codeExample string
switch req.Language {
case "go":
if req.Complexity == "simple" {
codeExample = `
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}`
} else {
codeExample = `
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Printf("Hello, World! Current time is %s\n", now.Format(time.RFC3339))
}`
}
case "python":
// Python example code
if req.Complexity == "simple" {
codeExample = `
def greet(name):
return f"Hello, {name}!"
print(greet("World"))`
} else {
codeExample = `
import datetime
def greet(name, include_time=False):
message = f"Hello, {name}!"
if include_time:
message += f" Current time is {datetime.datetime.now().isoformat()}"
return message
print(greet("World", include_time=True))`
}
}
// Create messages array according to MCP spec
messages := []mcp.PromptMessage{
{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: fmt.Sprintf("You are a helpful coding assistant specialized in %s programming.", req.Language),
},
},
{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Text: fmt.Sprintf("Show me a %s example of a Hello World program in %s.", req.Complexity, req.Language),
},
},
{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: fmt.Sprintf("Here's a %s example in %s:\n\n```%s%s\n```\n\nHow can I help you implement this?",
req.Complexity, req.Language, req.Language, codeExample),
},
},
}
return messages, nil
},
})
```
### Registering Resources
Resources provide access to external content such as files or generated data.
#### Basic Resource Example:
```go
// Register a static resource
server.RegisterResource(mcp.Resource{
Name: "example-document",
URI: "file:///example/document.txt",
Description: "An example document",
MimeType: "text/plain",
Handler: func(ctx context.Context) (mcp.ResourceContent, error) {
return mcp.ResourceContent{
URI: "file:///example/document.txt",
MimeType: "text/plain",
Text: "This is an example document content.",
}, nil
},
})
```
#### Dynamic Resource with Code Example:
```go
// Register a Go code resource with dynamic handler
server.RegisterResource(mcp.Resource{
Name: "go-example",
URI: "file:///project/src/main.go",
Description: "A simple Go example with multiple files",
MimeType: "text/x-go",
Handler: func(ctx context.Context) (mcp.ResourceContent, error) {
// Return ResourceContent with all required fields
return mcp.ResourceContent{
URI: "file:///project/src/main.go",
MimeType: "text/x-go",
Text: "package main\n\nimport (\n\t\"fmt\"\n\t\"./greeting\"\n)\n\nfunc main() {\n\tfmt.Println(greeting.Hello(\"world\"))\n}",
}, nil
},
})
// Register a companion file for the above example
server.RegisterResource(mcp.Resource{
Name: "go-greeting",
URI: "file:///project/src/greeting/greeting.go",
Description: "A greeting package for the Go example",
MimeType: "text/x-go",
Handler: func(ctx context.Context) (mcp.ResourceContent, error) {
return mcp.ResourceContent{
URI: "file:///project/src/greeting/greeting.go",
MimeType: "text/x-go",
Text: "package greeting\n\nfunc Hello(name string) string {\n\treturn \"Hello, \" + name + \"!\"\n}",
}, nil
},
})
```
#### Binary Resource Example:
```go
// Register a binary resource (like an image)
server.RegisterResource(mcp.Resource{
Name: "example-image",
URI: "file:///example/image.png",
Description: "An example image",
MimeType: "image/png",
Handler: func(ctx context.Context) (mcp.ResourceContent, error) {
// Read image from file or generate it
imageData := "base64EncodedImageData..." // Base64 encoded image data
return mcp.ResourceContent{
URI: "file:///example/image.png",
MimeType: "image/png",
Blob: imageData, // For binary data
}, nil
},
})
```
### Using Resources in Prompts
You can embed resources in prompt responses to create rich interactions with proper MCP-compliant structure:
```go
// Register a prompt that embeds a resource
server.RegisterPrompt(mcp.Prompt{
Name: "resource-example",
Description: "A prompt that embeds a resource",
Arguments: []mcp.PromptArgument{
{
Name: "file_type",
Description: "Type of file to show (rust or go)",
Required: true,
},
},
Handler: func(ctx context.Context, args map[string]string) ([]mcp.PromptMessage, error) {
var req struct {
FileType string `json:"file_type"`
}
if err := mcp.ParseArguments(args, &req); err != nil {
return nil, fmt.Errorf("failed to parse args: %w", err)
}
var resourceURI, mimeType, fileContent string
if req.FileType == "rust" {
resourceURI = "file:///project/src/main.rs"
mimeType = "text/x-rust"
fileContent = "fn main() {\n println!(\"Hello world!\");\n}"
} else {
resourceURI = "file:///project/src/main.go"
mimeType = "text/x-go"
fileContent = "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}"
}
// Create message with embedded resource using proper MCP format
return []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Text: fmt.Sprintf("Can you explain this %s code?", req.FileType),
},
},
{
Role: mcp.RoleAssistant,
Content: mcp.EmbeddedResource{
Type: mcp.ContentTypeResource,
Resource: struct {
URI string `json:"uri"`
MimeType string `json:"mimeType"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"`
}{
URI: resourceURI,
MimeType: mimeType,
Text: fileContent,
},
},
},
{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: fmt.Sprintf("Above is a simple Hello World example in %s. Let me explain how it works.", req.FileType),
},
},
}, nil
},
})
```
### Multiple File Resources Example
```go
// Register a prompt that demonstrates embedding multiple resource files
server.RegisterPrompt(mcp.Prompt{
Name: "go-code-example",
Description: "A prompt that correctly embeds multiple resource files",
Arguments: []mcp.PromptArgument{
{
Name: "format",
Description: "How to format the code display",
},
},
Handler: func(ctx context.Context, args map[string]string) ([]mcp.PromptMessage, error) {
var req struct {
Format string `json:"format,optional"`
}
if err := mcp.ParseArguments(args, &req); err != nil {
return nil, fmt.Errorf("failed to parse args: %w", err)
}
// Get the Go code for multiple files
var mainGoText string = "package main\n\nimport (\n\t\"fmt\"\n\t\"./greeting\"\n)\n\nfunc main() {\n\tfmt.Println(greeting.Hello(\"world\"))\n}"
var greetingGoText string = "package greeting\n\nfunc Hello(name string) string {\n\treturn \"Hello, \" + name + \"!\"\n}"
// Create message with properly formatted embedded resource per MCP spec
messages := []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Text: "Show me a simple Go example with proper imports.",
},
},
{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: "Here's a simple Go example project:",
},
},
{
Role: mcp.RoleAssistant,
Content: mcp.EmbeddedResource{
Type: mcp.ContentTypeResource,
Resource: struct {
URI string `json:"uri"`
MimeType string `json:"mimeType"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"`
}{
URI: "file:///project/src/main.go",
MimeType: "text/x-go",
Text: mainGoText,
},
},
},
}
// Add explanation and additional file if requested
if req.Format == "with_explanation" {
messages = append(messages, mcp.PromptMessage{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: "This example demonstrates a simple Go application with modular structure. The main.go file imports from a local 'greeting' package that provides the Hello function.",
},
})
// Also show the greeting.go file with correct resource format
messages = append(messages, mcp.PromptMessage{
Role: mcp.RoleAssistant,
Content: mcp.EmbeddedResource{
Type: mcp.ContentTypeResource,
Resource: struct {
URI string `json:"uri"`
MimeType string `json:"mimeType"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"`
}{
URI: "file:///project/src/greeting/greeting.go",
MimeType: "text/x-go",
Text: greetingGoText,
},
},
})
}
return messages, nil
},
})
```
### Complete Application Example
Here's a complete example demonstrating all the components:
```go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/mcp"
)
func main() {
// Load configuration
var c mcp.McpConf
if err := conf.Load("config.yaml", &c); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Set up logging
logx.DisableStat()
// Create MCP server
server := mcp.NewMcpServer(c)
defer server.Stop()
// Register a simple echo tool
echoTool := mcp.Tool{
Name: "echo",
Description: "Echoes back the message provided by the user",
InputSchema: mcp.InputSchema{
Properties: map[string]any{
"message": map[string]any{
"type": "string",
"description": "The message to echo back",
},
"prefix": map[string]any{
"type": "string",
"description": "Optional prefix to add to the echoed message",
"default": "Echo: ",
},
},
Required: []string{"message"},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
var req struct {
Message string `json:"message"`
Prefix string `json:"prefix,optional"`
}
if err := mcp.ParseArguments(params, &req); err != nil {
return nil, fmt.Errorf("failed to parse args: %w", err)
}
prefix := "Echo: "
if len(req.Prefix) > 0 {
prefix = req.Prefix
}
return prefix + req.Message, nil
},
}
server.RegisterTool(echoTool)
// Register a static prompt
server.RegisterPrompt(mcp.Prompt{
Name: "greeting",
Description: "A simple greeting prompt",
Arguments: []mcp.PromptArgument{
{
Name: "name",
Description: "The name to greet",
Required: true,
},
},
Content: "Hello {{name}}! How can I assist you today?",
})
// Register a dynamic prompt
server.RegisterPrompt(mcp.Prompt{
Name: "dynamic-prompt",
Description: "A prompt that uses a handler to generate dynamic content",
Arguments: []mcp.PromptArgument{
{
Name: "username",
Description: "User's name for personalized greeting",
Required: true,
},
{
Name: "topic",
Description: "Topic of expertise",
Required: true,
},
},
Handler: func(ctx context.Context, args map[string]string) ([]mcp.PromptMessage, error) {
var req struct {
Username string `json:"username"`
Topic string `json:"topic"`
}
if err := mcp.ParseArguments(args, &req); err != nil {
return nil, fmt.Errorf("failed to parse args: %w", err)
}
// Create messages with current time
currentTime := time.Now().Format(time.RFC1123)
return []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.TextContent{
Text: fmt.Sprintf("Hello, I'm %s and I'd like to learn about %s.", req.Username, req.Topic),
},
},
{
Role: mcp.RoleAssistant,
Content: mcp.TextContent{
Text: fmt.Sprintf("Hello %s! I'm an AI assistant and I'll help you learn about %s. The current time is %s.",
req.Username, req.Topic, currentTime),
},
},
}, nil
},
})
// Register a resource
server.RegisterResource(mcp.Resource{
Name: "example-doc",
URI: "file:///example/doc.txt",
Description: "An example document",
MimeType: "text/plain",
Handler: func(ctx context.Context) (mcp.ResourceContent, error) {
return mcp.ResourceContent{
URI: "file:///example/doc.txt",
MimeType: "text/plain",
Text: "This is the content of the example document.",
}, nil
},
})
// Start the server
fmt.Printf("Starting MCP server on %s:%d\n", c.Host, c.Port)
server.Start()
}
```
## Error Handling
The MCP implementation provides comprehensive error handling:
- Tool execution errors are properly reported back to clients
- Missing or invalid parameters are detected and reported with appropriate error codes
- Resource and prompt lookup failures are handled gracefully
- Timeout handling for long-running tool executions using context
- Panic recovery to prevent server crashes
## Advanced Features
- **Annotations**: Add audience and priority metadata to content
- **Content Types**: Support for text, images, audio, and other content formats
- **Embedded Resources**: Include file resources directly in prompt responses
- **Context Awareness**: All handlers receive context.Context for timeout and cancellation support
- **Progress Tokens**: Support for tracking progress of long-running operations
- **Customizable Timeouts**: Configure execution timeouts for tools and operations
## Performance Considerations
- Tool execution runs with configurable timeouts to prevent blocking
- Efficient client tracking and cleanup to prevent resource leaks
- Proper concurrency handling with mutex protection for shared resources
- Buffered message channels to prevent blocking on client message delivery

940
mcp/server.go Normal file
View File

@@ -0,0 +1,940 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest"
)
func NewMcpServer(c McpConf) McpServer {
var server *rest.Server
if len(c.Mcp.Cors) == 0 {
server = rest.MustNewServer(c.RestConf)
} else {
server = rest.MustNewServer(c.RestConf, rest.WithCors(c.Mcp.Cors...))
}
if len(c.Mcp.Name) == 0 {
c.Mcp.Name = c.Name
}
if len(c.Mcp.BaseUrl) == 0 {
c.Mcp.BaseUrl = fmt.Sprintf("http://localhost:%d", c.Port)
}
s := &sseMcpServer{
conf: c,
server: server,
clients: make(map[string]*mcpClient),
tools: make(map[string]Tool),
prompts: make(map[string]Prompt),
resources: make(map[string]Resource),
}
// SSE endpoint for real-time updates
s.server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: s.conf.Mcp.SseEndpoint,
Handler: s.handleSSE,
}, rest.WithSSE(), rest.WithTimeout(c.Mcp.SseTimeout))
// JSON-RPC message endpoint for regular requests
s.server.AddRoute(rest.Route{
Method: http.MethodPost,
Path: s.conf.Mcp.MessageEndpoint,
Handler: s.handleRequest,
}, rest.WithTimeout(c.Mcp.MessageTimeout))
return s
}
// RegisterPrompt registers a new prompt with the server
func (s *sseMcpServer) RegisterPrompt(prompt Prompt) {
s.promptsLock.Lock()
s.prompts[prompt.Name] = prompt
s.promptsLock.Unlock()
// Notify clients about the new prompt
s.broadcast(eventPromptsListChanged, map[string][]Prompt{keyPrompts: {prompt}})
}
// RegisterResource registers a new resource with the server
func (s *sseMcpServer) RegisterResource(resource Resource) {
s.resourcesLock.Lock()
s.resources[resource.URI] = resource
s.resourcesLock.Unlock()
// Notify clients about the new resource
s.broadcast(eventResourcesListChanged, map[string][]Resource{keyResources: {resource}})
}
// RegisterTool registers a new tool with the server
func (s *sseMcpServer) RegisterTool(tool Tool) error {
if tool.Handler == nil {
return fmt.Errorf("tool '%s' has no handler function", tool.Name)
}
s.toolsLock.Lock()
s.tools[tool.Name] = tool
s.toolsLock.Unlock()
// Notify clients about the new tool
s.broadcast(eventToolsListChanged, map[string][]Tool{keyTools: {tool}})
return nil
}
// Start implements McpServer.
func (s *sseMcpServer) Start() {
s.server.Start()
}
func (s *sseMcpServer) Stop() {
s.server.Stop()
}
// broadcast sends a message to all connected clients
// It uses Server-Sent Events (SSE) format for real-time communication
func (s *sseMcpServer) broadcast(event string, data any) {
jsonData, err := json.Marshal(data)
if err != nil {
logx.Errorf("Failed to marshal broadcast data: %v", err)
return
}
// Lock only while reading the clients map
s.clientsLock.Lock()
clients := make([]*mcpClient, 0, len(s.clients))
for _, client := range s.clients {
clients = append(clients, client)
}
s.clientsLock.Unlock()
clientCount := len(clients)
if clientCount == 0 {
return
}
logx.Infof("Broadcasting event '%s' to %d clients", event, clientCount)
// Use CRLF line endings as per SSE specification
message := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", event, string(jsonData))
// Send messages without holding the lock
for _, client := range clients {
select {
case client.channel <- message:
// Message sent successfully
default:
// Channel buffer is full, log warning and continue
logx.Errorf("Client channel buffer full, dropping message for client %s", client.id)
}
}
}
// cleanupClient removes a client from the active clients map
func (s *sseMcpServer) cleanupClient(sessionId string) {
s.clientsLock.Lock()
defer s.clientsLock.Unlock()
if client, exists := s.clients[sessionId]; exists {
// Close the channel to signal any goroutines waiting on it
close(client.channel)
// Remove from active clients
delete(s.clients, sessionId)
logx.Infof("Cleaned up client %s (remaining clients: %d)", sessionId, len(s.clients))
}
}
// handleRequest handles MCP JSON-RPC requests
func (s *sseMcpServer) handleRequest(w http.ResponseWriter, r *http.Request) {
// Extract sessionId from query parameters
sessionId := r.URL.Query().Get(sessionIdKey)
if len(sessionId) == 0 {
http.Error(w, fmt.Sprintf("Missing %s", sessionIdKey), http.StatusBadRequest)
return
}
// Check if the client with this sessionId exists
s.clientsLock.Lock()
client, exists := s.clients[sessionId]
s.clientsLock.Unlock()
if !exists {
http.Error(w, fmt.Sprintf("Invalid or expired %s", sessionIdKey), http.StatusBadRequest)
return
}
var req Request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// For notification methods (no ID), we don't send a response
isNotification, err := req.isNotification()
if err != nil {
http.Error(w, "Invalid request.ID", http.StatusBadRequest)
}
w.WriteHeader(http.StatusAccepted)
// Special handling for initialization sequence
// Always allow initialize and notifications/initialized regardless of client state
if req.Method == methodInitialize {
logx.Infof("Processing initialize request with ID: %v", req.ID)
s.processInitialize(r.Context(), client, req)
logx.Infof("Sent initialize response for ID: %v, waiting for notifications/initialized", req.ID)
return
} else if req.Method == methodNotificationsInitialized {
// Handle initialized notification
logx.Info("Received notifications/initialized notification")
if !isNotification {
s.sendErrorResponse(r.Context(), client, req.ID,
"Method should be used as a notification", errCodeInvalidRequest)
return
}
s.processNotificationInitialized(client)
return
} else if !client.initialized && req.Method != methodNotificationsCancelled {
// Block most requests until client is initialized (except for cancellations)
s.sendErrorResponse(r.Context(), client, req.ID,
"Client not fully initialized, waiting for notifications/initialized",
errCodeClientNotInitialized)
return
}
// Process normal requests only after initialization
switch req.Method {
case methodToolsCall:
logx.Infof("Received tools call request with ID: %v", req.ID)
s.processToolCall(r.Context(), client, req)
logx.Infof("Sent tools call response for ID: %v", req.ID)
case methodToolsList:
logx.Infof("Processing tools/list request with ID: %v", req.ID)
s.processListTools(r.Context(), client, req)
logx.Infof("Sent tools/list response for ID: %v", req.ID)
case methodPromptsList:
logx.Infof("Processing prompts/list request with ID: %v", req.ID)
s.processListPrompts(r.Context(), client, req)
logx.Infof("Sent prompts/list response for ID: %v", req.ID)
case methodPromptsGet:
logx.Infof("Processing prompts/get request with ID: %v", req.ID)
s.processGetPrompt(r.Context(), client, req)
logx.Infof("Sent prompts/get response for ID: %v", req.ID)
case methodResourcesList:
logx.Infof("Processing resources/list request with ID: %v", req.ID)
s.processListResources(r.Context(), client, req)
logx.Infof("Sent resources/list response for ID: %v", req.ID)
case methodResourcesRead:
logx.Infof("Processing resources/read request with ID: %v", req.ID)
s.processResourcesRead(r.Context(), client, req)
logx.Infof("Sent resources/read response for ID: %v", req.ID)
case methodResourcesSubscribe:
logx.Infof("Processing resources/subscribe request with ID: %v", req.ID)
s.processResourceSubscribe(r.Context(), client, req)
logx.Infof("Sent resources/subscribe response for ID: %v", req.ID)
case methodPing:
logx.Infof("Processing ping request with ID: %v", req.ID)
s.processPing(r.Context(), client, req)
case methodNotificationsCancelled:
logx.Infof("Received notifications/cancelled notification: %v", req.ID)
s.processNotificationCancelled(r.Context(), client, req)
default:
logx.Infof("Unknown method: %s from client: %v", req.Method, req.ID)
s.sendErrorResponse(r.Context(), client, req.ID, "Method not found", errCodeMethodNotFound)
}
}
// handleSSE handles Server-Sent Events connections
func (s *sseMcpServer) handleSSE(w http.ResponseWriter, r *http.Request) {
// Generate a unique session ID for this client
sessionId := uuid.New().String()
// Create new client with buffered channel to prevent blocking
client := &mcpClient{
id: sessionId,
channel: make(chan string, eventChanSize),
}
// Add client to active clients map
s.clientsLock.Lock()
s.clients[sessionId] = client
activeClients := len(s.clients)
s.clientsLock.Unlock()
logx.Infof("New SSE connection established for client %s (active clients: %d)",
sessionId, activeClients)
// Set proper SSE headers
w.Header().Set("Transfer-Encoding", "chunked")
// Enable streaming
flusher, ok := w.(http.Flusher)
if !ok {
logx.Error("Streaming not supported by the underlying http.ResponseWriter")
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
return
}
// Send the message endpoint URL to the client
endpoint := fmt.Sprintf("%s%s?%s=%s",
s.conf.Mcp.BaseUrl, s.conf.Mcp.MessageEndpoint, sessionIdKey, sessionId)
// Format and send the endpoint message
endpointMsg := formatSSEMessage(eventEndpoint, []byte(endpoint))
if _, err := fmt.Fprint(w, endpointMsg); err != nil {
logx.Errorf("Failed to send endpoint message to client %s: %v", sessionId, err)
s.cleanupClient(sessionId)
return
}
flusher.Flush()
// Set up keep-alive ping and client cleanup
ticker := time.NewTicker(pingInterval.Load())
defer func() {
ticker.Stop()
s.cleanupClient(sessionId)
logx.Infof("SSE connection closed for client %s", sessionId)
}()
// Message processing loop
for {
select {
case message, ok := <-client.channel:
if !ok {
// Channel was closed, end connection
logx.Infof("Client channel was closed for %s", sessionId)
return
}
// Write message to the response
if _, err := fmt.Fprint(w, message); err != nil {
logx.Infof("Failed to write message to client %s: %v", sessionId, err)
return
}
flusher.Flush()
case <-ticker.C:
// Send keep-alive ping to maintain connection
ping := fmt.Sprintf(`{"type":"ping","timestamp":"%s"}`, time.Now().String())
pingMsg := formatSSEMessage("ping", []byte(ping))
if _, err := fmt.Fprint(w, pingMsg); err != nil {
logx.Errorf("Failed to send ping to client %s, closing connection: %v", sessionId, err)
return
}
flusher.Flush()
case <-r.Context().Done():
// Client disconnected or request was canceled or timed out
logx.Infof("Client %s disconnected: context done", sessionId)
return
}
}
}
// processInitialize processes the initialize request
func (s *sseMcpServer) processInitialize(ctx context.Context, client *mcpClient, req Request) {
// Create a proper JSON-RPC response that preserves the client's request ID
result := initializationResponse{
ProtocolVersion: s.conf.Mcp.ProtocolVersion,
Capabilities: capabilities{
Prompts: struct {
ListChanged bool `json:"listChanged"`
}{
ListChanged: true,
},
Resources: struct {
Subscribe bool `json:"subscribe"`
ListChanged bool `json:"listChanged"`
}{
Subscribe: true,
ListChanged: true,
},
Tools: struct {
ListChanged bool `json:"listChanged"`
}{
ListChanged: true,
},
},
ServerInfo: serverInfo{
Name: s.conf.Mcp.Name,
Version: s.conf.Mcp.Version,
},
}
// Mark client as initialized
client.initialized = true
// Send response with client's original request ID
s.sendResponse(ctx, client, req.ID, result)
}
// processListTools processes the tools/list request
func (s *sseMcpServer) processListTools(ctx context.Context, client *mcpClient, req Request) {
// Extract pagination params if any
var nextCursor string
var progressToken any
// Extract meta data including progress token
if req.Params != nil {
var metaParams struct {
Cursor string `json:"cursor"`
Meta struct {
ProgressToken any `json:"progressToken"`
} `json:"_meta"`
}
if err := json.Unmarshal(req.Params, &metaParams); err == nil {
if len(metaParams.Cursor) > 0 {
nextCursor = metaParams.Cursor
}
progressToken = metaParams.Meta.ProgressToken
}
}
var toolsList []Tool
s.toolsLock.Lock()
for _, tool := range s.tools {
if len(tool.InputSchema.Type) == 0 {
tool.InputSchema.Type = ContentTypeObject
}
toolsList = append(toolsList, tool)
}
s.toolsLock.Unlock()
result := ListToolsResult{
PaginatedResult: PaginatedResult{
Result: Result{},
NextCursor: Cursor(nextCursor),
},
Tools: toolsList,
}
// Add meta information if progress token was provided
if progressToken != nil {
result.Result.Meta = map[string]any{
progressTokenKey: progressToken,
}
}
s.sendResponse(ctx, client, req.ID, result)
}
// processListPrompts processes the prompts/list request
func (s *sseMcpServer) processListPrompts(ctx context.Context, client *mcpClient, req Request) {
// Extract pagination params if any
var nextCursor string
if req.Params != nil {
var cursorParams struct {
Cursor string `json:"cursor"`
}
if err := json.Unmarshal(req.Params, &cursorParams); err == nil && cursorParams.Cursor != "" {
// If we have a valid cursor, we could use it for pagination
// For now, we're not actually implementing pagination, so this is just
// to show how it would be extracted from the request
_ = cursorParams.Cursor
}
}
// Prepare prompt list
var promptsList []Prompt
s.promptsLock.Lock()
for _, prompt := range s.prompts {
promptsList = append(promptsList, prompt)
}
s.promptsLock.Unlock()
// In a real implementation, you'd handle pagination here
// For now, we'll return all prompts at once
result := struct {
Prompts []Prompt `json:"prompts"`
NextCursor string `json:"nextCursor,omitempty"`
Meta *struct{} `json:"_meta,omitempty"`
}{
Prompts: promptsList,
NextCursor: nextCursor,
}
s.sendResponse(ctx, client, req.ID, result)
}
// processListResources processes the resources/list request
func (s *sseMcpServer) processListResources(ctx context.Context, client *mcpClient, req Request) {
// Extract pagination params if any
var nextCursor string
var progressToken any
// Extract meta information including progress token if available
if req.Params != nil {
var metaParams PaginatedParams
if err := json.Unmarshal(req.Params, &metaParams); err == nil {
if len(metaParams.Cursor) > 0 {
nextCursor = metaParams.Cursor
}
progressToken = metaParams.Meta.ProgressToken
}
}
var resourcesList []Resource
s.resourcesLock.Lock()
for _, resource := range s.resources {
// Create a copy without the handler function which shouldn't be sent to clients
resourceCopy := Resource{
URI: resource.URI,
Name: resource.Name,
Description: resource.Description,
MimeType: resource.MimeType,
}
resourcesList = append(resourcesList, resourceCopy)
}
s.resourcesLock.Unlock()
// Create proper ResourcesListResult according to MCP specification
result := ResourcesListResult{
PaginatedResult: PaginatedResult{
Result: Result{},
NextCursor: Cursor(nextCursor),
},
Resources: resourcesList,
}
// Add meta information if progress token was provided
if progressToken != nil {
result.Result.Meta = map[string]any{
progressTokenKey: progressToken,
}
}
s.sendResponse(ctx, client, req.ID, result)
}
// processGetPrompt processes the prompts/get request
func (s *sseMcpServer) processGetPrompt(ctx context.Context, client *mcpClient, req Request) {
type GetPromptParams struct {
Name string `json:"name"`
Arguments map[string]string `json:"arguments,omitempty"`
}
var params GetPromptParams
if err := json.Unmarshal(req.Params, &params); err != nil {
s.sendErrorResponse(ctx, client, req.ID, "Invalid parameters", errCodeInvalidParams)
return
}
// Check if prompt exists
s.promptsLock.Lock()
prompt, exists := s.prompts[params.Name]
s.promptsLock.Unlock()
if !exists {
message := fmt.Sprintf("Prompt '%s' not found", params.Name)
s.sendErrorResponse(ctx, client, req.ID, message, errCodeInvalidParams)
return
}
logx.Infof("Processing prompt request: %s with %d arguments", prompt.Name, len(params.Arguments))
// Validate required arguments
missingArgs := validatePromptArguments(prompt, params.Arguments)
if len(missingArgs) > 0 {
message := fmt.Sprintf("Missing required arguments: %s", strings.Join(missingArgs, ", "))
s.sendErrorResponse(ctx, client, req.ID, message, errCodeInvalidParams)
return
}
// Ensure arguments are initialized to an empty map if nil
if params.Arguments == nil {
params.Arguments = make(map[string]string)
}
args := params.Arguments
// Generate messages using handler or static content
var messages []PromptMessage
var err error
if prompt.Handler != nil {
// Use dynamic handler to generate messages
messages, err = prompt.Handler(ctx, args)
if err != nil {
logx.Errorf("Error from prompt handler: %v", err)
s.sendErrorResponse(ctx, client, req.ID,
fmt.Sprintf("Error generating prompt content: %v", err), errCodeInternalError)
return
}
} else {
// No handler, generate messages from static content
var messageText string
if len(prompt.Content) > 0 {
messageText = prompt.Content
// Apply argument substitutions to static content
for key, value := range args {
placeholder := fmt.Sprintf("{{%s}}", key)
messageText = strings.Replace(messageText, placeholder, value, -1)
}
}
// Create a single user message with the content
messages = []PromptMessage{
{
Role: RoleUser,
Content: TextContent{
Text: messageText,
},
},
}
}
// Construct the response according to MCP spec
result := struct {
Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
}{
Description: prompt.Description,
Messages: toTypedPromptMessages(messages),
}
s.sendResponse(ctx, client, req.ID, result)
}
// processToolCall processes the tools/call request
func (s *sseMcpServer) processToolCall(ctx context.Context, client *mcpClient, req Request) {
var toolCallParams struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta struct {
ProgressToken any `json:"progressToken"`
} `json:"_meta,omitempty"`
}
// Handle different types of req.Params
// If it's a RawMessage (JSON), unmarshal it
if err := json.Unmarshal(req.Params, &toolCallParams); err != nil {
logx.Errorf("Failed to unmarshal tool call params: %v", err)
s.sendErrorResponse(ctx, client, req.ID, "Invalid tool call parameters", errCodeInvalidParams)
return
}
// Extract progress token if available
progressToken := toolCallParams.Meta.ProgressToken
// Find the requested tool
s.toolsLock.Lock()
tool, exists := s.tools[toolCallParams.Name]
s.toolsLock.Unlock()
if !exists {
s.sendErrorResponse(ctx, client, req.ID, fmt.Sprintf("Tool '%s' not found",
toolCallParams.Name), errCodeInvalidParams)
return
}
// Log parameters before execution
logx.Infof("Executing tool '%s' with arguments: %#v", toolCallParams.Name, toolCallParams.Arguments)
// Execute the tool handler with timeout handling
var result any
var err error
// Create a channel to receive the result
// make sure to have 1 size buffer to avoid channel leak if timeout
resultCh := make(chan struct {
result any
err error
}, 1)
// Execute the tool handler in a goroutine
go func() {
toolResult, toolErr := tool.Handler(ctx, toolCallParams.Arguments)
resultCh <- struct {
result any
err error
}{
result: toolResult,
err: toolErr,
}
}()
// Wait for either the result or a timeout
select {
case res := <-resultCh:
result = res.result
err = res.err
case <-ctx.Done():
// Handle request timeout
logx.Errorf("Tool execution timed out after %v: %s", s.conf.Mcp.MessageTimeout, toolCallParams.Name)
s.sendErrorResponse(ctx, client, req.ID, "Tool execution timed out", errCodeTimeout)
return
}
// Create the base result structure with metadata
callToolResult := CallToolResult{
Result: Result{},
Content: []any{},
IsError: false,
}
// Add meta information if progress token was provided
if progressToken != nil {
callToolResult.Result.Meta = map[string]any{
progressTokenKey: progressToken,
}
}
// Check if there was an error during tool execution
if err != nil {
// According to the spec, for tool-level errors (as opposed to protocol-level errors),
// we should report them inside the result with isError=true
logx.Errorf("Tool execution reported error: %v", err)
callToolResult.Content = []any{
TextContent{
Text: fmt.Sprintf("Error: %v", err),
},
}
callToolResult.IsError = true
s.sendResponse(ctx, client, req.ID, callToolResult)
return
}
// Format the response according to the CallToolResult schema
switch v := result.(type) {
case string:
// Simple string becomes text content
callToolResult.Content = append(callToolResult.Content, TextContent{
Text: v,
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
},
})
case map[string]any:
// JSON-like object becomes formatted JSON text
jsonStr, err := json.Marshal(v)
if err != nil {
jsonStr = []byte(err.Error())
}
callToolResult.Content = append(callToolResult.Content, TextContent{
Text: string(jsonStr),
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
},
})
case TextContent:
callToolResult.Content = append(callToolResult.Content, v)
case ImageContent:
callToolResult.Content = append(callToolResult.Content, v)
case []any:
callToolResult.Content = v
case ToolResult:
// Handle legacy ToolResult type
switch v.Type {
case ContentTypeText:
callToolResult.Content = append(callToolResult.Content, TextContent{
Text: fmt.Sprintf("%v", v.Content),
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
},
})
case ContentTypeImage:
if imgData, ok := v.Content.(map[string]any); ok {
callToolResult.Content = append(callToolResult.Content, ImageContent{
Data: fmt.Sprintf("%v", imgData["data"]),
MimeType: fmt.Sprintf("%v", imgData["mimeType"]),
})
}
default:
callToolResult.Content = append(callToolResult.Content, TextContent{
Text: fmt.Sprintf("%v", v.Content),
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
},
})
}
default:
// For any other type, convert to string
callToolResult.Content = append(callToolResult.Content, TextContent{
Text: fmt.Sprintf("%v", v),
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
},
})
}
callToolResult.Content = toTypedContents(callToolResult.Content)
logx.Infof("Tool call result: %#v", callToolResult)
s.sendResponse(ctx, client, req.ID, callToolResult)
}
// processResourcesRead processes the resources/read request
func (s *sseMcpServer) processResourcesRead(ctx context.Context, client *mcpClient, req Request) {
var params ResourceReadParams
if err := json.Unmarshal(req.Params, &params); err != nil {
s.sendErrorResponse(ctx, client, req.ID, "Invalid parameters", errCodeInvalidParams)
return
}
// Find resource that matches the URI
s.resourcesLock.Lock()
resource, exists := s.resources[params.URI]
s.resourcesLock.Unlock()
if !exists {
s.sendErrorResponse(ctx, client, req.ID, fmt.Sprintf("Resource with URI '%s' not found",
params.URI), errCodeResourceNotFound)
return
}
// If no handler is provided, return an empty content array
if resource.Handler == nil {
result := ResourceReadResult{
Contents: []ResourceContent{
{
URI: params.URI,
MimeType: resource.MimeType,
Text: "",
},
},
}
s.sendResponse(ctx, client, req.ID, result)
return
}
// Execute the resource handler
content, err := resource.Handler(ctx)
if err != nil {
s.sendErrorResponse(ctx, client, req.ID, fmt.Sprintf("Error reading resource: %v", err),
errCodeInternalError)
return
}
// Ensure the URI is set if not already provided by the handler
if len(content.URI) == 0 {
content.URI = params.URI
}
// Ensure MimeType is set if available from the resource definition
if len(content.MimeType) == 0 && len(resource.MimeType) > 0 {
content.MimeType = resource.MimeType
}
// Create response with contents from the handler
// The MCP specification requires a contents array
result := ResourceReadResult{
Contents: []ResourceContent{content},
}
s.sendResponse(ctx, client, req.ID, result)
}
// processResourceSubscribe processes the resources/subscribe request
func (s *sseMcpServer) processResourceSubscribe(ctx context.Context, client *mcpClient, req Request) {
var params ResourceSubscribeParams
if err := json.Unmarshal(req.Params, &params); err != nil {
s.sendErrorResponse(ctx, client, req.ID, "Invalid parameters", errCodeInvalidParams)
return
}
// Check if the resource exists
s.resourcesLock.Lock()
_, exists := s.resources[params.URI]
s.resourcesLock.Unlock()
if !exists {
s.sendErrorResponse(ctx, client, req.ID, fmt.Sprintf("Resource with URI '%s' not found",
params.URI), errCodeResourceNotFound)
return
}
// Send success response for the subscription
s.sendResponse(ctx, client, req.ID, struct{}{})
}
// processNotificationCancelled processes the notifications/cancelled notification
func (s *sseMcpServer) processNotificationCancelled(ctx context.Context, client *mcpClient, req Request) {
// Extract the requestId that was canceled
type CancelParams struct {
RequestId int64 `json:"requestId"`
Reason string `json:"reason"`
}
var params CancelParams
if err := json.Unmarshal(req.Params, &params); err != nil {
logx.Errorf("Failed to parse cancellation params: %v", err)
return
}
logx.Infof("Request %d was cancelled by client. Reason: %s", params.RequestId, params.Reason)
}
// processNotificationInitialized processes the notifications/initialized notification
func (s *sseMcpServer) processNotificationInitialized(client *mcpClient) {
// Mark the client as properly initialized
client.initialized = true
logx.Infof("Client %s is now fully initialized and ready for normal operations", client.id)
}
// processPing processes the ping request and responds immediately
func (s *sseMcpServer) processPing(ctx context.Context, client *mcpClient, req Request) {
// A ping request should simply respond with an empty result to confirm the server is alive
logx.Infof("Received ping request with ID: %d", req.ID)
// Send an empty response with client's original request ID
s.sendResponse(ctx, client, req.ID, struct{}{})
}
// sendErrorResponse sends an error response via the SSE channel
func (s *sseMcpServer) sendErrorResponse(ctx context.Context, client *mcpClient,
id any, message string, code int) {
errorResponse := struct {
JsonRpc string `json:"jsonrpc"`
ID any `json:"id"`
Error errorMessage `json:"error"`
}{
JsonRpc: jsonRpcVersion,
ID: id,
Error: errorMessage{
Code: code,
Message: message,
},
}
// all fields are primitive types, impossible to fail
jsonData, _ := json.Marshal(errorResponse)
// Use CRLF line endings as requested
sseMessage := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", eventMessage, string(jsonData))
logx.Infof("Sending error for ID %v: %s", id, sseMessage)
// cannot receive from ctx.Done() because we're sending to the channel for SSE messages
select {
case client.channel <- sseMessage:
default:
// Channel buffer is full, log warning and continue
logx.Infof("Client %s channel is full while sending error response with ID %d", client.id, id)
}
}
// sendResponse sends a success response via the SSE channel
func (s *sseMcpServer) sendResponse(ctx context.Context, client *mcpClient, id any, result any) {
response := Response{
JsonRpc: jsonRpcVersion,
ID: id,
Result: result,
}
jsonData, err := json.Marshal(response)
if err != nil {
s.sendErrorResponse(ctx, client, id, "Failed to marshal response", errCodeInternalError)
return
}
// Use CRLF line endings as requested
sseMessage := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", eventMessage, string(jsonData))
logx.Infof("Sending response for ID %v: %s", id, sseMessage)
// cannot receive from ctx.Done() because we're sending to the channel for SSE messages
select {
case client.channel <- sseMessage:
default:
// Channel buffer is full, log warning and continue
logx.Infof("Client %s channel is full while sending response with ID %v", client.id, id)
}
}

3451
mcp/server_test.go Normal file

File diff suppressed because it is too large Load Diff

317
mcp/types.go Normal file
View File

@@ -0,0 +1,317 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"sync"
"github.com/zeromicro/go-zero/rest"
)
// Cursor is an opaque token used for pagination
type Cursor string
// Request represents a generic MCP request following JSON-RPC 2.0 specification
type Request struct {
SessionId string `form:"session_id"` // Session identifier for client tracking
JsonRpc string `json:"jsonrpc"` // Must be "2.0" per JSON-RPC spec
ID any `json:"id"` // Request identifier for matching responses
Method string `json:"method"` // Method name to invoke
Params json.RawMessage `json:"params"` // Parameters for the method
}
func (r Request) isNotification() (bool, error) {
switch val := r.ID.(type) {
case int:
return val == 0, nil
case int64:
return val == 0, nil
case float64:
return val == 0.0, nil
case string:
return len(val) == 0, nil
case nil:
return true, nil
default:
return false, fmt.Errorf("invalid type %T", val)
}
}
type PaginatedParams struct {
Cursor string `json:"cursor"`
Meta struct {
ProgressToken any `json:"progressToken"`
} `json:"_meta"`
}
// Result is the base interface for all results
type Result struct {
Meta map[string]any `json:"_meta,omitempty"` // Optional metadata
}
// PaginatedResult is a base for results that support pagination
type PaginatedResult struct {
Result
NextCursor Cursor `json:"nextCursor,omitempty"` // Opaque token for fetching next page
}
// ListToolsResult represents the response to a tools/list request
type ListToolsResult struct {
PaginatedResult
Tools []Tool `json:"tools"` // List of available tools
}
// Message Content Types
// RoleType represents the sender or recipient of messages in a conversation
type RoleType string
// PromptArgument defines a single argument that can be passed to a prompt
type PromptArgument struct {
Name string `json:"name"` // Argument name
Description string `json:"description,omitempty"` // Human-readable description
Required bool `json:"required,omitempty"` // Whether this argument is required
}
// PromptHandler is a function that dynamically generates prompt content
type PromptHandler func(ctx context.Context, args map[string]string) ([]PromptMessage, error)
// Prompt represents an MCP Prompt definition
type Prompt struct {
Name string `json:"name"` // Unique identifier for the prompt
Description string `json:"description,omitempty"` // Human-readable description
Arguments []PromptArgument `json:"arguments,omitempty"` // Arguments for customization
Content string `json:"-"` // Static content (internal use only)
Handler PromptHandler `json:"-"` // Handler for dynamic content generation
}
// PromptMessage represents a message in a conversation
type PromptMessage struct {
Role RoleType `json:"role"` // Message sender role
Content any `json:"content"` // Message content (TextContent, ImageContent, etc.)
}
// TextContent represents text content in a message
type TextContent struct {
Text string `json:"text"` // The text content
Annotations *Annotations `json:"annotations,omitempty"` // Optional annotations
}
type typedTextContent struct {
Type string `json:"type"`
TextContent
}
// ImageContent represents image data in a message
type ImageContent struct {
Data string `json:"data"` // Base64-encoded image data
MimeType string `json:"mimeType"` // MIME type (e.g., "image/png")
}
type typedImageContent struct {
Type string `json:"type"`
ImageContent
}
// AudioContent represents audio data in a message
type AudioContent struct {
Data string `json:"data"` // Base64-encoded audio data
MimeType string `json:"mimeType"` // MIME type (e.g., "audio/mp3")
}
type typedAudioContent struct {
Type string `json:"type"`
AudioContent
}
// FileContent represents file content
type FileContent struct {
URI string `json:"uri"` // URI identifying the file
MimeType string `json:"mimeType"` // MIME type of the file
Text string `json:"text"` // File content as text
}
// EmbeddedResource represents a resource embedded in a message
type EmbeddedResource struct {
Type string `json:"type"` // Always "resource"
Resource ResourceContent `json:"resource"` // The resource data
}
// Annotations provides additional metadata for content
type Annotations struct {
Audience []RoleType `json:"audience,omitempty"` // Who should see this content
Priority *float64 `json:"priority,omitempty"` // Optional priority (0-1)
}
// Tool-related Types
// ToolHandler is a function that handles tool calls
type ToolHandler func(ctx context.Context, params map[string]any) (any, error)
// Tool represents a Model Context Protocol Tool definition
type Tool struct {
Name string `json:"name"` // Unique identifier for the tool
Description string `json:"description"` // Human-readable description
InputSchema InputSchema `json:"inputSchema"` // JSON Schema for parameters
Handler ToolHandler `json:"-"` // Not sent to clients
}
// InputSchema represents tool's input schema in JSON Schema format
type InputSchema struct {
Type string `json:"type"`
Properties map[string]any `json:"properties"` // Property definitions
Required []string `json:"required,omitempty"` // List of required properties
}
// CallToolResult represents a tool call result that conforms to the MCP schema
type CallToolResult struct {
Result
Content []any `json:"content"` // Content items (text, images, etc.)
IsError bool `json:"isError,omitempty"` // True if tool execution failed
}
// Resource represents a Model Context Protocol Resource definition
type Resource struct {
URI string `json:"uri"` // Unique resource identifier (RFC3986)
Name string `json:"name"` // Human-readable name
Description string `json:"description,omitempty"` // Optional description
MimeType string `json:"mimeType,omitempty"` // Optional MIME type
Handler ResourceHandler `json:"-"` // Internal handler not sent to clients
}
// ResourceHandler is a function that handles resource read requests
type ResourceHandler func(ctx context.Context) (ResourceContent, error)
// ResourceContent represents the content of a resource
type ResourceContent struct {
URI string `json:"uri"` // Resource URI (required)
MimeType string `json:"mimeType,omitempty"` // MIME type of the resource
Text string `json:"text,omitempty"` // Text content (if available)
Blob string `json:"blob,omitempty"` // Base64 encoded blob data (if available)
}
// ResourcesListResult represents the response to a resources/list request
type ResourcesListResult struct {
PaginatedResult
Resources []Resource `json:"resources"` // List of available resources
}
// ResourceReadParams contains parameters for a resources/read request
type ResourceReadParams struct {
URI string `json:"uri"` // URI of the resource to read
}
// ResourceReadResult contains the result of a resources/read request
type ResourceReadResult struct {
Result
Contents []ResourceContent `json:"contents"` // Array of resource content
}
// ResourceSubscribeParams contains parameters for a resources/subscribe request
type ResourceSubscribeParams struct {
URI string `json:"uri"` // URI of the resource to subscribe to
}
// ResourceUpdateNotification represents a notification about a resource update
type ResourceUpdateNotification struct {
URI string `json:"uri"` // URI of the updated resource
Content ResourceContent `json:"content"` // New resource content
}
// Client and Server Types
// mcpClient represents an SSE client connection
type mcpClient struct {
id string // Unique client identifier
channel chan string // Channel for sending SSE messages
initialized bool // Tracks if client has sent notifications/initialized
}
// McpServer defines the interface for Model Context Protocol servers
type McpServer interface {
Start()
Stop()
RegisterTool(tool Tool) error
RegisterPrompt(prompt Prompt)
RegisterResource(resource Resource)
}
// sseMcpServer implements the McpServer interface using SSE
type sseMcpServer struct {
conf McpConf
server *rest.Server
clients map[string]*mcpClient
clientsLock sync.Mutex
tools map[string]Tool
toolsLock sync.Mutex
prompts map[string]Prompt
promptsLock sync.Mutex
resources map[string]Resource
resourcesLock sync.Mutex
}
// Response Types
// errorObj represents a JSON-RPC error object
type errorObj struct {
Code int `json:"code"` // Error code
Message string `json:"message"` // Error message
}
// Response represents a JSON-RPC response
type Response struct {
JsonRpc string `json:"jsonrpc"` // Always "2.0"
ID any `json:"id"` // Same as request ID
Result any `json:"result"` // Result object (null if error)
Error *errorObj `json:"error,omitempty"` // Error object (null if success)
}
// Server Information Types
// serverInfo provides information about the server
type serverInfo struct {
Name string `json:"name"` // Server name
Version string `json:"version"` // Server version
}
// capabilities describes the server's capabilities
type capabilities struct {
Logging struct{} `json:"logging"`
Prompts struct {
ListChanged bool `json:"listChanged"` // Server will notify on prompt changes
} `json:"prompts"`
Resources struct {
Subscribe bool `json:"subscribe"` // Server supports resource subscriptions
ListChanged bool `json:"listChanged"` // Server will notify on resource changes
} `json:"resources"`
Tools struct {
ListChanged bool `json:"listChanged"` // Server will notify on tool changes
} `json:"tools"`
}
// initializationResponse is sent in response to an initialize request
type initializationResponse struct {
ProtocolVersion string `json:"protocolVersion"` // Protocol version
Capabilities capabilities `json:"capabilities"` // Server capabilities
ServerInfo serverInfo `json:"serverInfo"` // Server information
}
// ToolCallParams contains the parameters for a tool call
type ToolCallParams struct {
Name string `json:"name"` // Tool name
Parameters map[string]any `json:"parameters"` // Tool parameters
}
// ToolResult contains the result of a tool execution
type ToolResult struct {
Type string `json:"type"` // Content type (text, image, etc.)
Content any `json:"content"` // Result content
}
// errorMessage represents a detailed error message
type errorMessage struct {
Code int `json:"code"` // Error code
Message string `json:"message"` // Error message
Data any `json:",omitempty"` // Additional error data
}

271
mcp/types_test.go Normal file
View File

@@ -0,0 +1,271 @@
package mcp
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestResponseMarshaling(t *testing.T) {
// Test that the Response struct marshals correctly
resp := Response{
JsonRpc: "2.0",
ID: 123,
Result: map[string]string{
"key": "value",
},
}
data, err := json.Marshal(resp)
assert.NoError(t, err)
assert.Contains(t, string(data), `"jsonrpc":"2.0"`)
assert.Contains(t, string(data), `"id":123`)
assert.Contains(t, string(data), `"result":{"key":"value"}`)
// Test response with error
respWithError := Response{
JsonRpc: "2.0",
ID: 456,
Error: &errorObj{
Code: errCodeInvalidRequest,
Message: "Invalid Request",
},
}
data, err = json.Marshal(respWithError)
assert.NoError(t, err)
assert.Contains(t, string(data), `"jsonrpc":"2.0"`)
assert.Contains(t, string(data), `"id":456`)
assert.Contains(t, string(data), `"error":{"code":-32600,"message":"Invalid Request"}`)
}
func TestRequestUnmarshaling(t *testing.T) {
// Test that the Request struct unmarshals correctly
jsonStr := `{
"jsonrpc": "2.0",
"id": 789,
"method": "test_method",
"params": {"key": "value"}
}`
var req Request
err := json.Unmarshal([]byte(jsonStr), &req)
assert.NoError(t, err)
assert.Equal(t, "2.0", req.JsonRpc)
assert.Equal(t, float64(789), req.ID)
assert.Equal(t, "test_method", req.Method)
// Check params unmarshaled correctly
var params map[string]string
err = json.Unmarshal(req.Params, &params)
assert.NoError(t, err)
assert.Equal(t, "value", params["key"])
}
func TestToolStructs(t *testing.T) {
// Test Tool struct
tool := Tool{
Name: "test.tool",
Description: "A test tool",
InputSchema: InputSchema{
Type: "object",
Properties: map[string]any{
"input": map[string]any{
"type": "string",
"description": "Input parameter",
},
},
Required: []string{"input"},
},
Handler: func(ctx context.Context, params map[string]any) (any, error) {
return "result", nil
},
}
// Verify fields are correct
assert.Equal(t, "test.tool", tool.Name)
assert.Equal(t, "A test tool", tool.Description)
assert.Equal(t, "object", tool.InputSchema.Type)
assert.Contains(t, tool.InputSchema.Properties, "input")
propMap, ok := tool.InputSchema.Properties["input"].(map[string]any)
assert.True(t, ok, "Property should be a map")
assert.Equal(t, "string", propMap["type"])
assert.NotNil(t, tool.Handler)
// Verify JSON marshalling (which should exclude Handler function)
data, err := json.Marshal(tool)
assert.NoError(t, err)
assert.Contains(t, string(data), `"name":"test.tool"`)
assert.Contains(t, string(data), `"description":"A test tool"`)
assert.Contains(t, string(data), `"inputSchema":`)
assert.NotContains(t, string(data), `"Handler":`)
}
func TestPromptStructs(t *testing.T) {
// Test Prompt struct
prompt := Prompt{
Name: "test.prompt",
Description: "A test prompt description",
}
// Verify fields are correct
assert.Equal(t, "test.prompt", prompt.Name)
assert.Equal(t, "A test prompt description", prompt.Description)
// Verify JSON marshalling
data, err := json.Marshal(prompt)
assert.NoError(t, err)
assert.Contains(t, string(data), `"name":"test.prompt"`)
assert.Contains(t, string(data), `"description":"A test prompt description"`)
}
func TestResourceStructs(t *testing.T) {
// Test Resource struct
resource := Resource{
Name: "test.resource",
URI: "http://example.com/resource",
Description: "A test resource",
}
// Verify fields are correct
assert.Equal(t, "test.resource", resource.Name)
assert.Equal(t, "http://example.com/resource", resource.URI)
assert.Equal(t, "A test resource", resource.Description)
// Verify JSON marshalling
data, err := json.Marshal(resource)
assert.NoError(t, err)
assert.Contains(t, string(data), `"name":"test.resource"`)
assert.Contains(t, string(data), `"uri":"http://example.com/resource"`)
assert.Contains(t, string(data), `"description":"A test resource"`)
}
func TestContentTypes(t *testing.T) {
// Test TextContent
textContent := TextContent{
Text: "Sample text",
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
Priority: ptr(1.0),
},
}
data, err := json.Marshal(textContent)
assert.NoError(t, err)
assert.Contains(t, string(data), `"text":"Sample text"`)
assert.Contains(t, string(data), `"audience":["user","assistant"]`)
assert.Contains(t, string(data), `"priority":1`)
// Test ImageContent
imageContent := ImageContent{
Data: "base64data",
MimeType: "image/png",
}
data, err = json.Marshal(imageContent)
assert.NoError(t, err)
assert.Contains(t, string(data), `"data":"base64data"`)
assert.Contains(t, string(data), `"mimeType":"image/png"`)
// Test AudioContent
audioContent := AudioContent{
Data: "base64audio",
MimeType: "audio/mp3",
}
data, err = json.Marshal(audioContent)
assert.NoError(t, err)
assert.Contains(t, string(data), `"data":"base64audio"`)
assert.Contains(t, string(data), `"mimeType":"audio/mp3"`)
}
func TestCallToolResult(t *testing.T) {
// Test CallToolResult
result := CallToolResult{
Result: Result{
Meta: map[string]any{
"progressToken": "token123",
},
},
Content: []interface{}{
TextContent{
Text: "Sample result",
},
},
IsError: false,
}
data, err := json.Marshal(result)
assert.NoError(t, err)
assert.Contains(t, string(data), `"_meta":{"progressToken":"token123"}`)
assert.Contains(t, string(data), `"content":[{"text":"Sample result"}]`)
assert.NotContains(t, string(data), `"isError":`)
}
func TestRequest_isNotification(t *testing.T) {
tests := []struct {
name string
id any
want bool
wantErr error
}{
// integer test cases
{name: "int zero", id: 0, want: true, wantErr: nil},
{name: "int non-zero", id: 1, want: false, wantErr: nil},
{name: "int64 zero", id: int64(0), want: true, wantErr: nil},
{name: "int64 max", id: int64(9223372036854775807), want: false, wantErr: nil},
// floating point number test cases
{name: "float64 zero", id: float64(0.0), want: true, wantErr: nil},
{name: "float64 positive", id: float64(0.000001), want: false, wantErr: nil},
{name: "float64 negative", id: float64(-0.000001), want: false, wantErr: nil},
{name: "float64 epsilon", id: float64(1e-300), want: false, wantErr: nil},
// string test cases
{name: "empty string", id: "", want: true, wantErr: nil},
{name: "non-empty string", id: "abc", want: false, wantErr: nil},
{name: "space string", id: " ", want: false, wantErr: nil},
{name: "unicode string", id: "こんにちは", want: false, wantErr: nil},
// special cases
{name: "nil", id: nil, want: true, wantErr: nil},
// logical type test cases
{name: "bool true", id: true, want: false, wantErr: errors.New("invalid type bool")},
{name: "bool false", id: false, want: false, wantErr: errors.New("invalid type bool")},
{name: "struct type", id: struct{}{}, want: false, wantErr: errors.New("invalid type struct {}")},
{name: "slice type", id: []int{1, 2, 3}, want: false, wantErr: errors.New("invalid type []int")},
{name: "map type", id: map[string]int{"a": 1}, want: false, wantErr: errors.New("invalid type map[string]int")},
{name: "pointer type", id: new(int), want: false, wantErr: errors.New("invalid type *int")},
{name: "func type", id: func() {}, want: false, wantErr: errors.New("invalid type func()")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := Request{
SessionId: "test-session",
JsonRpc: "2.0",
ID: tt.id,
Method: "testMethod",
Params: json.RawMessage(`{}`),
}
got, err := req.isNotification()
if (err != nil) != (tt.wantErr != nil) {
t.Fatalf("error presence mismatch: got error = %v, wantErr %v", err, tt.wantErr)
}
if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() {
t.Fatalf("error message mismatch:\ngot %q\nwant %q", err.Error(), tt.wantErr.Error())
}
if got != tt.want {
t.Errorf("isNotification() = %v, want %v for ID %v (%T)", got, tt.want, tt.id, tt.id)
}
})
}
}

107
mcp/util.go Normal file
View File

@@ -0,0 +1,107 @@
package mcp
import "fmt"
// formatSSEMessage formats a Server-Sent Event message with proper CRLF line endings
func formatSSEMessage(event string, data []byte) string {
return fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", event, string(data))
}
// ptr is a helper function to get a pointer to a value
func ptr[T any](v T) *T {
return &v
}
func toTypedContents(contents []any) []any {
typedContents := make([]any, len(contents))
for i, content := range contents {
switch v := content.(type) {
case TextContent:
typedContents[i] = typedTextContent{
Type: ContentTypeText,
TextContent: v,
}
case ImageContent:
typedContents[i] = typedImageContent{
Type: ContentTypeImage,
ImageContent: v,
}
case AudioContent:
typedContents[i] = typedAudioContent{
Type: ContentTypeAudio,
AudioContent: v,
}
default:
typedContents[i] = typedTextContent{
Type: ContentTypeText,
TextContent: TextContent{
Text: fmt.Sprintf("Unknown content type: %T", v),
},
}
}
}
return typedContents
}
func toTypedPromptMessages(messages []PromptMessage) []PromptMessage {
typedMessages := make([]PromptMessage, len(messages))
for i, msg := range messages {
switch v := msg.Content.(type) {
case TextContent:
typedMessages[i] = PromptMessage{
Role: msg.Role,
Content: typedTextContent{
Type: ContentTypeText,
TextContent: v,
},
}
case ImageContent:
typedMessages[i] = PromptMessage{
Role: msg.Role,
Content: typedImageContent{
Type: ContentTypeImage,
ImageContent: v,
},
}
case AudioContent:
typedMessages[i] = PromptMessage{
Role: msg.Role,
Content: typedAudioContent{
Type: ContentTypeAudio,
AudioContent: v,
},
}
default:
typedMessages[i] = PromptMessage{
Role: msg.Role,
Content: typedTextContent{
Type: ContentTypeText,
TextContent: TextContent{
Text: fmt.Sprintf("Unknown content type: %T", v),
},
},
}
}
}
return typedMessages
}
// validatePromptArguments checks if all required arguments are provided
// Returns a list of missing required arguments
func validatePromptArguments(prompt Prompt, providedArgs map[string]string) []string {
var missingArgs []string
for _, arg := range prompt.Arguments {
if arg.Required {
if value, exists := providedArgs[arg.Name]; !exists || len(value) == 0 {
missingArgs = append(missingArgs, arg.Name)
}
}
}
return missingArgs
}

274
mcp/util_test.go Normal file
View File

@@ -0,0 +1,274 @@
package mcp
import (
"bufio"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type Event struct {
Type string
Data map[string]any
}
func parseEvent(input string) (*Event, error) {
var evt Event
var dataStr string
scanner := bufio.NewScanner(strings.NewReader(input))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "event:") {
evt.Type = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
dataStr = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(dataStr) > 0 {
if err := json.Unmarshal([]byte(dataStr), &evt.Data); err != nil {
return nil, fmt.Errorf("failed to parse data: %w", err)
}
}
return &evt, nil
}
// TestToTypedPromptMessages tests the toTypedPromptMessages function
func TestToTypedPromptMessages(t *testing.T) {
// Test with multiple message types in one test
t.Run("MixedContentTypes", func(t *testing.T) {
// Create test data with different content types
messages := []PromptMessage{
{
Role: RoleUser,
Content: TextContent{
Text: "Hello, this is a text message",
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
Priority: ptr(0.8),
},
},
},
{
Role: RoleAssistant,
Content: ImageContent{
Data: "base64ImageData",
MimeType: "image/jpeg",
},
},
{
Role: RoleUser,
Content: AudioContent{
Data: "base64AudioData",
MimeType: "audio/mp3",
},
},
{
Role: "system",
Content: "This is a simple string that should be handled as unknown type",
},
}
// Call the function
result := toTypedPromptMessages(messages)
// Validate results
require.Len(t, result, 4, "Should return the same number of messages")
// Validate first message (TextContent)
msg := result[0]
assert.Equal(t, RoleUser, msg.Role, "Role should be preserved")
// Type assertion using reflection since Content is an interface
typed, ok := msg.Content.(typedTextContent)
require.True(t, ok, "Should be typedTextContent")
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
assert.Equal(t, "Hello, this is a text message", typed.Text, "Text content should be preserved")
require.NotNil(t, typed.Annotations, "Annotations should be preserved")
assert.Equal(t, []RoleType{RoleUser, RoleAssistant}, typed.Annotations.Audience, "Audience should be preserved")
require.NotNil(t, typed.Annotations.Priority, "Priority should be preserved")
assert.Equal(t, 0.8, *typed.Annotations.Priority, "Priority value should be preserved")
// Validate second message (ImageContent)
msg = result[1]
assert.Equal(t, RoleAssistant, msg.Role, "Role should be preserved")
// Type assertion for image content
typedImg, ok := msg.Content.(typedImageContent)
require.True(t, ok, "Should be typedImageContent")
assert.Equal(t, ContentTypeImage, typedImg.Type, "Type should be image")
assert.Equal(t, "base64ImageData", typedImg.Data, "Image data should be preserved")
assert.Equal(t, "image/jpeg", typedImg.MimeType, "MimeType should be preserved")
// Validate third message (AudioContent)
msg = result[2]
assert.Equal(t, RoleUser, msg.Role, "Role should be preserved")
// Type assertion for audio content
typedAudio, ok := msg.Content.(typedAudioContent)
require.True(t, ok, "Should be typedAudioContent")
assert.Equal(t, ContentTypeAudio, typedAudio.Type, "Type should be audio")
assert.Equal(t, "base64AudioData", typedAudio.Data, "Audio data should be preserved")
assert.Equal(t, "audio/mp3", typedAudio.MimeType, "MimeType should be preserved")
// Validate fourth message (unknown type converted to TextContent)
msg = result[3]
assert.Equal(t, RoleType("system"), msg.Role, "Role should be preserved")
// Should be converted to a typedTextContent with error message
typedUnknown, ok := msg.Content.(typedTextContent)
require.True(t, ok, "Unknown content should be converted to typedTextContent")
assert.Equal(t, ContentTypeText, typedUnknown.Type, "Type should be text")
assert.Contains(t, typedUnknown.Text, "Unknown content type:", "Should contain error about unknown type")
assert.Contains(t, typedUnknown.Text, "string", "Should mention the actual type")
})
// Test empty input
t.Run("EmptyInput", func(t *testing.T) {
messages := []PromptMessage{}
result := toTypedPromptMessages(messages)
assert.Empty(t, result, "Should return empty slice for empty input")
})
// Test with nil annotations
t.Run("NilAnnotations", func(t *testing.T) {
messages := []PromptMessage{
{
Role: RoleUser,
Content: TextContent{
Text: "Text with nil annotations",
Annotations: nil,
},
},
}
result := toTypedPromptMessages(messages)
require.Len(t, result, 1, "Should return one message")
typed, ok := result[0].Content.(typedTextContent)
require.True(t, ok, "Should be typedTextContent")
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
assert.Equal(t, "Text with nil annotations", typed.Text, "Text content should be preserved")
assert.Nil(t, typed.Annotations, "Nil annotations should be preserved as nil")
})
}
// TestToTypedContents tests the toTypedContents function
func TestToTypedContents(t *testing.T) {
// Test with multiple content types in one test
t.Run("MixedContentTypes", func(t *testing.T) {
// Create test data with different content types
contents := []any{
TextContent{
Text: "Hello, this is a text content",
Annotations: &Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
Priority: ptr(0.7),
},
},
ImageContent{
Data: "base64ImageData",
MimeType: "image/png",
},
AudioContent{
Data: "base64AudioData",
MimeType: "audio/wav",
},
"This is a simple string that should be handled as unknown type",
}
// Call the function
result := toTypedContents(contents)
// Validate results
require.Len(t, result, 4, "Should return the same number of contents")
// Validate first content (TextContent)
typed, ok := result[0].(typedTextContent)
require.True(t, ok, "Should be typedTextContent")
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
assert.Equal(t, "Hello, this is a text content", typed.Text, "Text content should be preserved")
require.NotNil(t, typed.Annotations, "Annotations should be preserved")
assert.Equal(t, []RoleType{RoleUser, RoleAssistant}, typed.Annotations.Audience, "Audience should be preserved")
require.NotNil(t, typed.Annotations.Priority, "Priority should be preserved")
assert.Equal(t, 0.7, *typed.Annotations.Priority, "Priority value should be preserved")
// Validate second content (ImageContent)
typedImg, ok := result[1].(typedImageContent)
require.True(t, ok, "Should be typedImageContent")
assert.Equal(t, ContentTypeImage, typedImg.Type, "Type should be image")
assert.Equal(t, "base64ImageData", typedImg.Data, "Image data should be preserved")
assert.Equal(t, "image/png", typedImg.MimeType, "MimeType should be preserved")
// Validate third content (AudioContent)
typedAudio, ok := result[2].(typedAudioContent)
require.True(t, ok, "Should be typedAudioContent")
assert.Equal(t, ContentTypeAudio, typedAudio.Type, "Type should be audio")
assert.Equal(t, "base64AudioData", typedAudio.Data, "Audio data should be preserved")
assert.Equal(t, "audio/wav", typedAudio.MimeType, "MimeType should be preserved")
// Validate fourth content (unknown type converted to TextContent)
typedUnknown, ok := result[3].(typedTextContent)
require.True(t, ok, "Unknown content should be converted to typedTextContent")
assert.Equal(t, ContentTypeText, typedUnknown.Type, "Type should be text")
assert.Contains(t, typedUnknown.Text, "Unknown content type:", "Should contain error about unknown type")
assert.Contains(t, typedUnknown.Text, "string", "Should mention the actual type")
})
// Test empty input
t.Run("EmptyInput", func(t *testing.T) {
contents := []any{}
result := toTypedContents(contents)
assert.Empty(t, result, "Should return empty slice for empty input")
})
// Test with nil annotations
t.Run("NilAnnotations", func(t *testing.T) {
contents := []any{
TextContent{
Text: "Text with nil annotations",
Annotations: nil,
},
}
result := toTypedContents(contents)
require.Len(t, result, 1, "Should return one content")
typed, ok := result[0].(typedTextContent)
require.True(t, ok, "Should be typedTextContent")
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
assert.Equal(t, "Text with nil annotations", typed.Text, "Text content should be preserved")
assert.Nil(t, typed.Annotations, "Nil annotations should be preserved as nil")
})
// Test with custom struct (should be handled as unknown type)
t.Run("CustomStruct", func(t *testing.T) {
type CustomContent struct {
Data string
}
contents := []any{
CustomContent{
Data: "custom data",
},
}
result := toTypedContents(contents)
require.Len(t, result, 1, "Should return one content")
typed, ok := result[0].(typedTextContent)
require.True(t, ok, "Custom struct should be converted to typedTextContent")
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
assert.Contains(t, typed.Text, "Unknown content type:", "Should contain error about unknown type")
assert.Contains(t, typed.Text, "CustomContent", "Should mention the actual type")
})
}

149
mcp/vars.go Normal file
View File

@@ -0,0 +1,149 @@
package mcp
import (
"time"
"github.com/zeromicro/go-zero/core/syncx"
)
// Protocol constants
const (
// JSON-RPC version as defined in the specification
jsonRpcVersion = "2.0"
// Session identifier key used in request URLs
sessionIdKey = "session_id"
// progressTokenKey is used to track progress of long-running tasks
progressTokenKey = "progressToken"
)
// Server-Sent Events (SSE) event types
const (
// Standard message event for JSON-RPC responses
eventMessage = "message"
// Endpoint event for sending endpoint URL to clients
eventEndpoint = "endpoint"
)
// Content type identifiers
const (
// ContentTypeObject is object content type
ContentTypeObject = "object"
// ContentTypeText is text content type
ContentTypeText = "text"
// ContentTypeImage is image content type
ContentTypeImage = "image"
// ContentTypeAudio is audio content type
ContentTypeAudio = "audio"
// ContentTypeResource is resource content type
ContentTypeResource = "resource"
)
// Collection keys for broadcast events
const (
// Key for prompts collection
keyPrompts = "prompts"
// Key for resources collection
keyResources = "resources"
// Key for tools collection
keyTools = "tools"
)
// JSON-RPC error codes
// Standard error codes from JSON-RPC 2.0 spec
const (
// Invalid JSON was received by the server
errCodeInvalidRequest = -32600
// The method does not exist / is not available
errCodeMethodNotFound = -32601
// Invalid method parameter(s)
errCodeInvalidParams = -32602
// Internal JSON-RPC error
errCodeInternalError = -32603
// Tool execution timed out
errCodeTimeout = -32001
// Resource not found error
errCodeResourceNotFound = -32002
// Client hasn't completed initialization
errCodeClientNotInitialized = -32800
)
// User and assistant role definitions
const (
// RoleUser is the "user" role - the entity asking questions
RoleUser RoleType = "user"
// RoleAssistant is the "assistant" role - the entity providing responses
RoleAssistant RoleType = "assistant"
)
// Method names as defined in the MCP specification
const (
// Initialize the connection between client and server
methodInitialize = "initialize"
// List available tools
methodToolsList = "tools/list"
// Call a specific tool
methodToolsCall = "tools/call"
// List available prompts
methodPromptsList = "prompts/list"
// Get a specific prompt
methodPromptsGet = "prompts/get"
// List available resources
methodResourcesList = "resources/list"
// Read a specific resource
methodResourcesRead = "resources/read"
// Subscribe to resource updates
methodResourcesSubscribe = "resources/subscribe"
// Simple ping to check server availability
methodPing = "ping"
// Notification that client is fully initialized
methodNotificationsInitialized = "notifications/initialized"
// Notification that a request was canceled
methodNotificationsCancelled = "notifications/cancelled"
)
// Event names for Server-Sent Events (SSE)
const (
// Notification of tool list changes
eventToolsListChanged = "tools/list_changed"
// Notification of prompt list changes
eventPromptsListChanged = "prompts/list_changed"
// Notification of resource list changes
eventResourcesListChanged = "resources/list_changed"
)
var (
// Default channel size for events
eventChanSize = 10
// Default ping interval for checking connection availability
// use syncx.ForAtomicDuration to ensure atomicity in test race
pingInterval = syncx.ForAtomicDuration(30 * time.Second)
)

210
mcp/vars_test.go Normal file
View File

@@ -0,0 +1,210 @@
// filepath: /Users/kevin/Develop/go/opensource/go-zero/mcp/vars_test.go
package mcp
import (
"encoding/json"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// TestErrorCodes ensures error codes are applied correctly in error responses
func TestErrorCodes(t *testing.T) {
testCases := []struct {
name string
code int
message string
expected string
}{
{
name: "invalid request error",
code: errCodeInvalidRequest,
message: "Invalid request",
expected: `"code":-32600`,
},
{
name: "method not found error",
code: errCodeMethodNotFound,
message: "Method not found",
expected: `"code":-32601`,
},
{
name: "invalid params error",
code: errCodeInvalidParams,
message: "Invalid parameters",
expected: `"code":-32602`,
},
{
name: "internal error",
code: errCodeInternalError,
message: "Internal server error",
expected: `"code":-32603`,
},
{
name: "timeout error",
code: errCodeTimeout,
message: "Operation timed out",
expected: `"code":-32001`,
},
{
name: "resource not found error",
code: errCodeResourceNotFound,
message: "Resource not found",
expected: `"code":-32002`,
},
{
name: "client not initialized error",
code: errCodeClientNotInitialized,
message: "Client not initialized",
expected: `"code":-32800`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
resp := Response{
JsonRpc: jsonRpcVersion,
ID: int64(1),
Error: &errorObj{
Code: tc.code,
Message: tc.message,
},
}
data, err := json.Marshal(resp)
assert.NoError(t, err)
assert.Contains(t, string(data), tc.expected, "Error code should match expected value")
assert.Contains(t, string(data), tc.message, "Error message should be included")
assert.Contains(t, string(data), jsonRpcVersion, "JSON-RPC version should be included")
})
}
}
// TestJsonRpcVersion ensures the correct JSON-RPC version is used
func TestJsonRpcVersion(t *testing.T) {
assert.Equal(t, "2.0", jsonRpcVersion, "JSON-RPC version should be 2.0")
// Test that it's used in responses
resp := Response{
JsonRpc: jsonRpcVersion,
ID: int64(1),
Result: "test",
}
data, err := json.Marshal(resp)
assert.NoError(t, err)
assert.Contains(t, string(data), `"jsonrpc":"2.0"`, "Response should use correct JSON-RPC version")
// Test that it's expected in requests
reqStr := `{"jsonrpc":"2.0","id":1,"method":"test"}`
var req Request
err = json.Unmarshal([]byte(reqStr), &req)
assert.NoError(t, err)
assert.Equal(t, jsonRpcVersion, req.JsonRpc, "Request should parse correct JSON-RPC version")
}
// TestSessionIdKey ensures session ID extraction works correctly
func TestSessionIdKey(t *testing.T) {
// Create a mock server implementation
mock := newMockMcpServer(t)
defer mock.shutdown()
// Verify the key constant
assert.Equal(t, "session_id", sessionIdKey, "Session ID key should be 'session_id'")
// Test that session ID is extracted correctly
mockR := httptest.NewRequest("GET", "/?"+sessionIdKey+"=test-session", nil)
// Since the mock server is using the same session key logic,
// we can test this by accessing the request query parameters directly
sessionID := mockR.URL.Query().Get(sessionIdKey)
assert.Equal(t, "test-session", sessionID, "Session ID should be extracted correctly")
}
// TestEventTypes ensures event types are set correctly in SSE responses
func TestEventTypes(t *testing.T) {
// Test message event
assert.Equal(t, "message", eventMessage, "Message event should be 'message'")
// Test endpoint event
assert.Equal(t, "endpoint", eventEndpoint, "Endpoint event should be 'endpoint'")
// Verify them in an actual SSE format string
messageEvent := "event: " + eventMessage + "\ndata: test\n\n"
assert.Contains(t, messageEvent, "event: message", "Message event should format correctly")
endpointEvent := "event: " + eventEndpoint + "\ndata: test\n\n"
assert.Contains(t, endpointEvent, "event: endpoint", "Endpoint event should format correctly")
}
// TestCollectionKeys checks that collection keys are used correctly
func TestCollectionKeys(t *testing.T) {
// Verify collection key constants
assert.Equal(t, "prompts", keyPrompts, "Prompts key should be 'prompts'")
assert.Equal(t, "resources", keyResources, "Resources key should be 'resources'")
assert.Equal(t, "tools", keyTools, "Tools key should be 'tools'")
}
// TestRoleTypes checks that role types are used correctly
func TestRoleTypes(t *testing.T) {
// Test in annotations
annotations := Annotations{
Audience: []RoleType{RoleUser, RoleAssistant},
}
data, err := json.Marshal(annotations)
assert.NoError(t, err)
assert.Contains(t, string(data), `"audience":["user","assistant"]`, "Role types should marshal correctly")
}
// TestMethodNames checks that method names are used correctly
func TestMethodNames(t *testing.T) {
// Verify method name constants
methods := map[string]string{
"initialize": methodInitialize,
"tools/list": methodToolsList,
"tools/call": methodToolsCall,
"prompts/list": methodPromptsList,
"prompts/get": methodPromptsGet,
"resources/list": methodResourcesList,
"resources/read": methodResourcesRead,
"resources/subscribe": methodResourcesSubscribe,
"ping": methodPing,
"notifications/initialized": methodNotificationsInitialized,
"notifications/cancelled": methodNotificationsCancelled,
}
for expected, actual := range methods {
assert.Equal(t, expected, actual, "Method name should be "+expected)
}
// Test in a request
for methodName := range methods {
req := Request{
JsonRpc: jsonRpcVersion,
ID: int64(1),
Method: methodName,
}
data, err := json.Marshal(req)
assert.NoError(t, err)
assert.Contains(t, string(data), `"method":"`+methodName+`"`, "Method name should be used in requests")
}
}
// TestEventNames checks that event names are used correctly
func TestEventNames(t *testing.T) {
// Verify event name constants
events := map[string]string{
"tools/list_changed": eventToolsListChanged,
"prompts/list_changed": eventPromptsListChanged,
"resources/list_changed": eventResourcesListChanged,
}
for expected, actual := range events {
assert.Equal(t, expected, actual, "Event name should be "+expected)
}
// Test event names in SSE format
for _, eventName := range events {
sseEvent := "event: " + eventName + "\ndata: test\n\n"
assert.Contains(t, sseEvent, "event: "+eventName, "Event name should format correctly in SSE")
}
}

View File

@@ -298,6 +298,12 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
>99. 新华三技术有限公司
>100. 上海邑脉科技有限公司
>101. 上海巨瓴科技有限公司
>102. 深圳市兴海物联科技有限公司
>103. 爱芯元智半导体股份有限公司
>104. 杭州升恒科技有限公司
>105. 昆仑万维科技股份有限公司
>106. 无锡盛算信息技术有限公司
>107. 深圳市聚货通信息科技有限公司
如果贵公司也已使用 go-zero欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。

View File

@@ -43,7 +43,6 @@ go-zero contains simple API description syntax and code generation tool called `
## Backgrounds of go-zero
At the beginning of 2018, we decided to re-design our system, from monolithic architecture with Java+MongoDB to microservice architecture. After research and comparison, we chose to:
In early 2018, we embarked on a transformative journey to redesign our system, transitioning from a monolithic architecture built with Java and MongoDB to a microservices architecture. After careful research and comparison, we made a deliberate choice to:
* Go Beyond with Golang
@@ -251,8 +250,4 @@ go-zero enlisted in the [CNCF Cloud Native Landscape](https://landscape.cncf.io/
## Give a Star! ⭐
If you like or are using this project to learn or start your solution, please give it a star. Thanks!
## Buy me a coffee
<a href="https://www.buymeacoffee.com/kevwan" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
If you like this project or are using it to learn or start your own solution, give it a star to get updates on new releases. Your support matters!

Some files were not shown because too many files have changed in this diff Show More