Compare commits

...

310 Commits

Author SHA1 Message Date
Kevin Wan
1f6688e5c1 chore: refactor the imports (#2406) 2022-09-17 20:06:23 +08:00
dawn_zhou
ae7f1aabdd feat: mysql and redis metric support (#2355)
* feat: mysql and redis metric support

* feat: mysql and redis metric support

* feat: mysql and redis metric support

Co-authored-by: dawn.zhou <dawn.zhou@yijinin.com>
2022-09-17 19:35:30 +08:00
dependabot[bot]
b8664be2bb chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc (#2402)
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-17 00:14:39 +08:00
dependabot[bot]
6e16a9647e chore(deps): bump go.etcd.io/etcd/client/v3 from 3.5.4 to 3.5.5 (#2395)
Bumps [go.etcd.io/etcd/client/v3](https://github.com/etcd-io/etcd) from 3.5.4 to 3.5.5.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Changelog](https://github.com/etcd-io/etcd/blob/main/Dockerfile-release.amd64)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.4...v3.5.5)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/client/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-09-16 23:54:05 +08:00
dependabot[bot]
bb0e76be47 chore(deps): bump go.etcd.io/etcd/api/v3 from 3.5.4 to 3.5.5 (#2394)
Bumps [go.etcd.io/etcd/api/v3](https://github.com/etcd-io/etcd) from 3.5.4 to 3.5.5.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Changelog](https://github.com/etcd-io/etcd/blob/main/Dockerfile-release.amd64)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.4...v3.5.5)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-15 23:07:56 +08:00
dependabot[bot]
27a20e1ed3 chore(deps): bump github.com/jhump/protoreflect from 1.12.0 to 1.13.0 (#2393)
Bumps [github.com/jhump/protoreflect](https://github.com/jhump/protoreflect) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/jhump/protoreflect/releases)
- [Commits](https://github.com/jhump/protoreflect/compare/v1.12.0...v1.13.0)

---
updated-dependencies:
- dependency-name: github.com/jhump/protoreflect
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-15 22:32:30 +08:00
dependabot[bot]
cbbbee0ace chore(deps): bump go.opentelemetry.io/otel/exporters/jaeger (#2389)
Bumps [go.opentelemetry.io/otel/exporters/jaeger](https://github.com/open-telemetry/opentelemetry-go) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/jaeger
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-15 22:00:07 +08:00
Kevin Wan
e9650d547b chore: refactor (#2388) 2022-09-14 23:46:34 +08:00
dependabot[bot]
60160f56b8 chore(deps): bump go.opentelemetry.io/otel/exporters/zipkin (#2385)
Bumps [go.opentelemetry.io/otel/exporters/zipkin](https://github.com/open-telemetry/opentelemetry-go) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/zipkin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-09-14 23:46:15 +08:00
genewoo
05c2f313c7 feat: add grpc export (#2379)
Co-authored-by: Gene Wu <gene.wu@cabital.com>
2022-09-14 22:54:52 +08:00
dependabot[bot]
f2a0f78288 chore(deps): bump go.opentelemetry.io/otel/sdk from 1.9.0 to 1.10.0 (#2383)
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-14 18:08:37 +08:00
Kevin Wan
3e96994b7b feat: support targetPort option in goctl kube (#2378) 2022-09-12 20:42:41 +08:00
Kevin Wan
66c2a28e66 fix #2364 (#2377) 2022-09-12 19:29:43 +08:00
Kevin Wan
9672071b5d Update readme-cn.md 2022-09-12 18:31:42 +08:00
anqiansong
9581e8445a fix: issue #2359 (#2368)
* Revert changes

* Unrap nested structure for doc code generation

* Revert changes

* Remove useless code

* Remove useless code

* Format code
2022-09-11 22:56:53 +08:00
dependabot[bot]
6ec8bc6655 chore(deps): bump github.com/lib/pq from 1.10.6 to 1.10.7 (#2373)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.6 to 1.10.7.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.6...v1.10.7)

---
updated-dependencies:
- dependency-name: github.com/lib/pq
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-09-11 22:33:37 +08:00
Kevin Wan
d935c83a54 feat: support baggage propagation in httpc (#2375)
* feat: support baggage propagation in httpc

* chore: use go 1.16

* chore: use go 1.16

* chore: use go ^1.16

* chore: remove deprecated
2022-09-10 15:18:52 +08:00
dependabot[bot]
590d784800 chore(deps): bump go.uber.org/goleak from 1.1.12 to 1.2.0 (#2371)
Bumps [go.uber.org/goleak](https://github.com/uber-go/goleak) from 1.1.12 to 1.2.0.
- [Release notes](https://github.com/uber-go/goleak/releases)
- [Changelog](https://github.com/uber-go/goleak/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/goleak/compare/v1.1.12...v1.2.0)

---
updated-dependencies:
- dependency-name: go.uber.org/goleak
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-08 12:07:50 +08:00
dependabot[bot]
784276b360 chore(deps): bump go.mongodb.org/mongo-driver from 1.10.1 to 1.10.2 (#2370) 2022-09-08 07:26:55 +08:00
Kevin Wan
da80662b0f chore: refactor (#2365) 2022-09-07 11:18:52 +08:00
maizige
cfda972d50 fix:trace graceful stop,pre loss trace (#2358) 2022-09-07 10:33:01 +08:00
Archer
6078bf1a04 correct test case (#2340) 2022-09-04 21:14:56 +08:00
anqiansong
ce638d26d9 Hidden java (#2333) 2022-08-30 23:54:36 +08:00
maizige
422f401153 fix:etcd get&watch not atomic (#2321) 2022-08-29 08:35:31 +08:00
Kevin Wan
dfeef5e497 fix: thread-safe in getWriter of logx (#2319) 2022-08-29 08:32:17 +08:00
Archer
8c72136631 make logx#getWriter concurrency-safe (#2233)
* make logx#getWriter concurrency-safe

* make logx#getWriter concurrency-safe
2022-08-28 22:10:50 +08:00
Zlx
9d6c8f67f5 generates nested types in doc (#2201)
Co-authored-by: Link_Zhao <Link_Zhao@epam.com>
2022-08-28 21:51:27 +08:00
anqiansong
f70805ee60 Add strict flag (#2248)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-08-28 18:55:52 +08:00
Kevin Wan
a1466e1707 fix: range validation on mapping (#2317) 2022-08-28 17:49:26 +08:00
lowang-bh
1b477bbef9 improve: number range compare left and righ value (#2315)
Co-authored-by: wanglonghui7 <wanglonghui7@jd.com>
2022-08-28 17:17:22 +08:00
Kevin Wan
813625d995 refactor: sequential range over safemap (#2316) 2022-08-28 17:16:31 +08:00
李平平
15a2802f12 safemap add Range method (#2314) 2022-08-28 16:51:45 +08:00
Kevin Wan
5d00dfb962 fix: handle the scenarios that content-length is invalid (#2313) 2022-08-28 15:41:02 +08:00
Kevin Wan
d9620bb072 chore: remove unused packages (#2312) 2022-08-28 14:20:03 +08:00
Kevin Wan
d978563523 fix: more accurate panic message on mapreduce (#2311) 2022-08-27 22:47:25 +08:00
yiGmMk
fb6d7e2fd2 fix #2301,package conflict generated by ddl (#2307)
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-08-27 21:56:39 +08:00
Kevin Wan
2d60f0c65a fix: logx disable not working in some cases (#2306)
* fix: logx disable not working in some cases

* fix: test fail
2022-08-27 19:24:31 +08:00
maizige
5d4ae201d0 Fix/del server interceptor duplicate copy md 20220827 (#2309)
* fix:grpc server interceptor duplicate copy MD

* modify wrong comments
2022-08-27 18:55:40 +08:00
maizige
05007c86bb fix:duplicate copy MD (#2304) 2022-08-27 12:18:23 +08:00
Kevin Wan
93584c6ca6 chore: refactor gateway (#2303) 2022-08-27 11:39:42 +08:00
dependabot[bot]
22bb7e95fd chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.3 to 2.0.5 (#2305)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.3 to 2.0.5.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.3...v2.0.5)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-27 11:39:07 +08:00
sado
bebf6322ff fix resource manager dead lock (#2302)
Co-authored-by: sado <liaoyonglin@bilibili.com>
2022-08-26 20:07:25 +08:00
Kevin Wan
36678f9023 chore: refactor stat (#2299) 2022-08-25 23:37:32 +08:00
Josh Quintana
90cdd61efc Initialize CPU stat code only if used (#2020)
Co-authored-by: Josh Quintana <josh@highwaybenefits.com>
2022-08-25 22:05:29 +08:00
dependabot[bot]
28166dedd6 chore(deps): bump google.golang.org/grpc from 1.48.0 to 1.49.0 (#2297) 2022-08-25 08:44:09 +08:00
chen quan
0316b6e10e feat(redis): add ZaddFloat & ZaddFloatCtx (#2291) 2022-08-24 21:02:16 +08:00
Kevin Wan
4cb68a034a fix #2163 (#2283) 2022-08-24 20:19:53 +08:00
chen quan
847a396f1c fix(logx): display garbled characters in windows(DOS, Powershell) (#2232)
* fix(logx): display garbled characters in windows(DOS, Powershell)

* Update writer.go
2022-08-23 22:45:11 +08:00
chen quan
c1babdf8b2 doc(readme): add star history (#2275) 2022-08-23 22:42:03 +08:00
MarkJoyMa
040c9e0954 feat: rpc add health check function configuration optional (#2288)
* feat: rpc add health check function configuration optional

* update config field name
2022-08-23 13:44:21 +08:00
Kevin Wan
1c85d39add Update readme-cn.md 2022-08-22 17:12:26 +08:00
Kevin Wan
4cd065f4f4 Update issues.yml 2022-08-19 23:10:16 +08:00
anqiansong
b9c97678bc chore: Update readme (#2280)
* Update readme

* Update readme
2022-08-19 23:08:07 +08:00
Kevin Wan
5208def65a fix #2240 (#2271) 2022-08-18 23:10:04 +08:00
Kevin Wan
3b96dc1598 Update readme-cn.md 2022-08-18 21:25:08 +08:00
dependabot[bot]
fa3f1bc19c chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.2 to 2.0.3 (#2267)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-17 22:30:27 +08:00
Kevin Wan
8ed22eafdd fix #2240 (#2263) 2022-08-14 19:49:47 +08:00
Kevin Wan
05dd6bd743 chore: refactor logx (#2262) 2022-08-14 13:58:06 +08:00
dependabot[bot]
9af1a42386 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.22.0 to 2.23.0 (#2260)
Bumps [github.com/alicebob/miniredis/v2](https://github.com/alicebob/miniredis) from 2.22.0 to 2.23.0.
- [Release notes](https://github.com/alicebob/miniredis/releases)
- [Changelog](https://github.com/alicebob/miniredis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/alicebob/miniredis/compare/v2.22.0...v2.23.0)

---
updated-dependencies:
- dependency-name: github.com/alicebob/miniredis/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-13 10:34:23 +08:00
Kevin Wan
f3645e420e test: add more tests (#2261) 2022-08-13 10:31:23 +08:00
fyyang
62abac0b7e fix: unsignedTypeMap type error (#2246) 2022-08-11 22:56:00 +08:00
Kevin Wan
6357e27418 fix: test failure, due to go 1.19 compatibility (#2256) 2022-08-11 22:55:12 +08:00
Kevin Wan
1568c3be0e fix: time repr wrapper (#2255) 2022-08-11 22:39:54 +08:00
dependabot[bot]
27e773fa1f chore(deps): bump github.com/prometheus/client_golang (#2244)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.12.2 to 1.13.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.12.2...v1.13.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-10 22:16:27 +08:00
dependabot[bot]
d8e17be33e chore(deps): bump github.com/fullstorydev/grpcurl from 1.8.6 to 1.8.7 (#2245)
Bumps [github.com/fullstorydev/grpcurl](https://github.com/fullstorydev/grpcurl) from 1.8.6 to 1.8.7.
- [Release notes](https://github.com/fullstorydev/grpcurl/releases)
- [Changelog](https://github.com/fullstorydev/grpcurl/blob/master/.goreleaser.yml)
- [Commits](https://github.com/fullstorydev/grpcurl/compare/v1.8.6...v1.8.7)

---
updated-dependencies:
- dependency-name: github.com/fullstorydev/grpcurl
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-10 22:09:04 +08:00
Kevin Wan
da5770ee2b chore: release action for goctl (#2239) 2022-08-07 16:49:22 +08:00
Kevin Wan
731b3ebf6f Update readme-cn.md 2022-08-07 16:11:43 +08:00
Kevin Wan
1e0f94ba86 Update readme.md 2022-08-07 16:11:27 +08:00
Kevin Wan
a987512c7b feat: more meaningful error messages, close body on httpc requests (#2238)
* feat: more meaningful error messages, close body on httpc requests

* fix: test failure
2022-08-07 16:09:54 +08:00
Kevin Wan
c1c7584de1 Update readme.md 2022-08-07 16:08:16 +08:00
Kevin Wan
98b9a25cc7 Update readme.md 2022-08-07 11:13:34 +08:00
Kevin Wan
a8305def3d docs: update docs for gateway (#2236) 2022-08-07 11:11:46 +08:00
Kevin Wan
d20d8324e7 fix: #2216 (#2235) 2022-08-06 17:48:59 +08:00
Kevin Wan
c638fce31c chore: renaming configs (#2234) 2022-08-06 16:32:12 +08:00
dependabot[bot]
34294702b0 chore(deps): bump go.mongodb.org/mongo-driver from 1.10.0 to 1.10.1 (#2225) 2022-08-04 20:25:56 +08:00
chen quan
4fad067a0e fix(logx): need to wait for the first caller to complete the execution. (#2213) 2022-08-03 23:59:39 +08:00
safeoy
3f3c811e08 fix: fix comment typo (#2220)
Use an instead of 'a' if the following word starts with a vowel sound, e.g. 'an in-memory cache'.
2022-08-03 23:57:49 +08:00
dependabot[bot]
dbdbb68676 chore(deps): bump go.opentelemetry.io/otel/exporters/zipkin (#2222)
Bumps [go.opentelemetry.io/otel/exporters/zipkin](https://github.com/open-telemetry/opentelemetry-go) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/zipkin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-08-03 23:56:22 +08:00
dependabot[bot]
83772344b0 chore(deps): bump go.opentelemetry.io/otel/exporters/jaeger (#2223)
Bumps [go.opentelemetry.io/otel/exporters/jaeger](https://github.com/open-telemetry/opentelemetry-go) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/jaeger
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-03 23:41:46 +08:00
Kevin Wan
49367f1713 fix: handling rpc error on gateway (#2212) 2022-08-01 00:01:24 +08:00
Kevin Wan
91b8effb24 chore: refactor redislock (#2210)
* chore: refactor redislock

* chore: add more tests
2022-07-30 19:46:10 +08:00
cong
4879d4dfcd feat(redislock): support set context (#2208)
* feat(redislock): support set context

* chore: fix test
2022-07-30 18:38:36 +08:00
dependabot[bot]
b18479dd43 chore(deps): bump google.golang.org/protobuf from 1.28.0 to 1.28.1 (#2205)
Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.28.0 to 1.28.1.
- [Release notes](https://github.com/protocolbuffers/protobuf-go/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash)
- [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.28.0...v1.28.1)

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-29 23:53:35 +08:00
Kevin Wan
5cd9229986 fix: only setup logx once (#2188)
* fix: only setup logx once

* fix: test failure

* chore: not reset logging level in reset

* chore: refactoring
2022-07-28 22:08:48 +08:00
施国鹏
3d38d36605 fix: logx test foo (#2144)
constant testlog "Stay hungry, stay foolish." contains foo(foolish), changed to foo1
2022-07-28 21:29:56 +08:00
chen quan
003adae51f fix(httpc): fix typo errors (#2189) 2022-07-27 09:11:15 +08:00
马守越
5348375b99 support mulitple protoset files (#2190) 2022-07-27 09:10:23 +08:00
benqi
5d7919a9f5 fix: remove invalid log fields in notLoggingContentMethods (#2187) 2022-07-24 22:18:04 +08:00
Kevin Wan
9b334b5428 chore: let logx.SetWriter can be called anytime (#2186) 2022-07-24 14:15:57 +08:00
fisnone
685d14e662 fix:duplicate route check (#2154)
Co-authored-by: 黄志荣 <huangzhirong@shuinfo.com>
2022-07-24 10:48:50 +08:00
benqi
edbf1a3b63 fix: fix switch doesn't work bug (#2183) 2022-07-23 12:15:37 +08:00
Kevin Wan
92145b56dc chore: refactoring (#2182) 2022-07-22 23:16:38 +08:00
Kevin Wan
34eb3fc12e chore: refactoring logx (#2181) 2022-07-22 22:28:01 +08:00
SgtDaJim
101304be53 feat: logx support logs rotation based on size limitation. (#1652) (#2167)
* feat: logx support logs rotation based on size limitation. (#1652)

implementation of #1652

Totally compatible with the old logx.LogConf. No effect if users do not change their options.

* feat: logx support logs rotation based on size limitation. (#1652)

implementation of #1652

Totally compatible with the old logx.LogConf. No effect if users do not change their options.

* feat: logx support logs rotation based on size limitation. (#1652)

implementation of #1652

Totally compatible with the old logx.LogConf. No effect if users do not change their options.

* feat: logx support logs rotation based on size limitation. (#1652)

implementation of #1652

Totally compatible with the old logx.LogConf. No effect if users do not change their options.
2022-07-22 21:13:10 +08:00
anqiansong
f630bc735b Update goctl version (#2178) 2022-07-21 15:29:50 +08:00
anqiansong
ca3c687f1c feat: Support for multiple rpc service generation and rpc grouping (#1972)
* Add group & compatible flag

* Add group & compatible flag

* Support for multiple rpc service generation and rpc grouping

* Support for multiple rpc service generation and rpc grouping

* Format code

* Format code

* Add comments

* Fix unit test

* Refactor function name

* Add example & Update grpc readme

* go mod tidy

* update mod

* update mod
2022-07-21 12:47:46 +08:00
anqiansong
1b51d0ce82 fix: fix #2102, #2108 (#2131)
* g4 code generation

* Update grammar

* g4 code generation

* fix #2108

* fix #2102

* Remove comments
2022-07-20 22:49:41 +08:00
Kevin Wan
d9218e1551 Update readme-cn.md
add go-zero users.
2022-07-20 09:40:32 +08:00
anqiansong
9c448c64ef Update api template (#2172) 2022-07-19 23:49:20 +08:00
杨圆建
bc85eaa9b1 fix: goctl genhandler duplicate rest/httpx & goctl genhandler template support custom import httpx package (#2152) 2022-07-19 23:24:47 +08:00
Kevin Wan
2a6f801978 chore: refactoring mapping name (#2168) 2022-07-19 09:58:46 +08:00
Kevin Wan
8d567b5508 feat: support customized header to metadata processor (#2162)
* chore: add more tests

* feat: support customized header processor
2022-07-17 23:21:19 +08:00
Kevin Wan
0dd2768d09 feat: support google.api.http in gateway (#2161) 2022-07-17 14:57:25 +08:00
Kevin Wan
4324ddc024 feat: set content-type to application/json (#2160) 2022-07-17 13:52:46 +08:00
Kevin Wan
557383fbbf feat: verify RpcPath on startup (#2159)
* feat: verify RpcPath on startup

* feat: support http header Grpc-Timeout
2022-07-17 12:37:23 +08:00
Kevin Wan
b206dd28a3 feat: support form values in gateway (#2158) 2022-07-16 23:40:53 +08:00
Kevin Wan
453fa309b1 feat: export gateway.Server to let users add middlewares (#2157) 2022-07-16 22:59:25 +08:00
Kevin Wan
4d7dae9cea Update readme-cn.md 2022-07-16 14:53:00 +08:00
Kevin Wan
d228b9038d Update readme.md 2022-07-16 14:52:45 +08:00
Kevin Wan
13477238a3 feat: restful -> grpc gateway (#2155)
* Revert "chore: remove unimplemented gateway (#2139)"

This reverts commit d70e73ec66.

* feat: working gateway

* feat: use mr to make it faster

* feat: working gateway

* chore: add comments

* feat: support protoset besides reflection

* feat: support zrpc client conf

* docs: update readme

* feat: support grpc-metadata- header to gateway- header conversion

* chore: add docs
2022-07-16 14:11:34 +08:00
dependabot[bot]
95a574e9e9 chore(deps): bump google.golang.org/grpc from 1.47.0 to 1.48.0 (#2147)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.47.0 to 1.48.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.47.0...v1.48.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-15 10:02:25 +08:00
dependabot[bot]
453100e0e2 chore(deps): bump go.mongodb.org/mongo-driver from 1.9.1 to 1.10.0 (#2150)
Bumps [go.mongodb.org/mongo-driver](https://github.com/mongodb/mongo-go-driver) from 1.9.1 to 1.10.0.
- [Release notes](https://github.com/mongodb/mongo-go-driver/releases)
- [Commits](https://github.com/mongodb/mongo-go-driver/compare/v1.9.1...v1.10.0)

---
updated-dependencies:
- dependency-name: go.mongodb.org/mongo-driver
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-15 10:02:08 +08:00
Kevin Wan
d70e73ec66 chore: remove unimplemented gateway (#2139) 2022-07-13 21:55:19 +08:00
Kevin Wan
300b124e42 docs: update goctl readme (#2136) 2022-07-12 23:16:40 +08:00
Kevin Wan
3bad043413 chore: refactor (#2130) 2022-07-11 23:50:50 +08:00
Kevin Wan
23f34234d0 chore: add more tests (#2129) 2022-07-11 23:32:57 +08:00
虫子樱桃
d71b3c841f feat:Add Routes method for server (#2125)
Co-authored-by: czyt <czyt@w.cn>
2022-07-11 23:23:38 +08:00
Kevin Wan
24787a946b feat: support logx.WithFields (#2128) 2022-07-11 23:19:26 +08:00
Richard Yi
6e50c87dca fix: generated sql query fields do not match template (#2004)
* Fix typo

* Match generated sql query fields with template
2022-07-11 23:06:00 +08:00
Kevin Wan
e672b3f8e1 feat: add Wrap and Wrapf in errorx (#2126) 2022-07-11 23:04:38 +08:00
Kevin Wan
1c09db6d5d chore: coding style (#2120) 2022-07-10 11:05:21 +08:00
LeeDF
96acf1f5a6 fix goctl rpc protoc strings.EqualFold Service.Name GoPackage (#2046) 2022-07-09 23:40:32 +08:00
Kevin Wan
97a171441d chore: remove blank lines (#2117)
* chore: remove blank lines

* chore: refactor
2022-07-09 15:59:25 +08:00
虫子樱桃
725e6056e1 feat:goctl model mongo add easy flag for easy declare. (#2073)
* fix:typo in readme.md

* feat:`goctl model mongo ` add `easy` flag to generate code with Auto generated CollectionName for easy declare.

* fix:`goctl api doc ` when referenced api file contains no route,will generate an empty markdown file.

* code: adjust code.

Co-authored-by: 虫子樱桃 <czyt@w.cn>
2022-07-09 15:34:01 +08:00
Kevin Wan
1410f7dc20 fix #2109 (#2116) 2022-07-09 15:05:59 +08:00
warrior
8afe68f3f1 refactor:remove duplicate codes (#2101)
Co-authored-by: 沈四胜 <sisheng.shen@71360.com>
2022-07-09 14:56:49 +08:00
dependabot[bot]
74c41e8c5e chore(deps): bump go.opentelemetry.io/otel/exporters/jaeger (#2115)
Bumps [go.opentelemetry.io/otel/exporters/jaeger](https://github.com/open-telemetry/opentelemetry-go) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/jaeger
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-07-09 14:42:16 +08:00
Minghong Fang
48f7e01158 feat: add method to jsonx (#2049) 2022-07-09 14:20:53 +08:00
dependabot[bot]
f6f6ee5c8c chore(deps): bump go.opentelemetry.io/otel/exporters/zipkin (#2112)
Bumps [go.opentelemetry.io/otel/exporters/zipkin](https://github.com/open-telemetry/opentelemetry-go) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/zipkin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-09 14:16:08 +08:00
Kevin Wan
b364c54940 chore: update goctl version to 1.3.9 (#2111) 2022-07-08 22:31:50 +08:00
Kevin Wan
e0e3f97c7c chore: refactor (#2087) 2022-07-02 14:03:11 +08:00
taobig
6a2d6786c6 remove legacy code (#2086) 2022-07-02 00:22:42 +08:00
Kevin Wan
18035bd4d4 chore: refactor (#2085) 2022-07-02 00:15:38 +08:00
家福
f3b8fef34f fix: type matching supports string to int (#2038)
* fix: type matching supports string to int

* feat: type matching supports string to int

Co-authored-by: 程家福 <chengjiafu@uniontech.com>
2022-07-01 23:21:31 +08:00
givemeafish
6a4885ba64 fix concurrent map writes (#2079)
Co-authored-by: wero <wero@werodeMacBook-Pro.local>
2022-07-01 23:07:25 +08:00
Kevin Wan
f2cef2b963 Update readme-cn.md 2022-07-01 23:00:19 +08:00
taobig
bfd0869ee2 remove legacy code (#2084) 2022-07-01 22:41:16 +08:00
Kevin Wan
4e26e0407e Update readme.md 2022-07-01 22:17:05 +08:00
wxc
d200ba4a7b feat: CompareAndSwapInt32 may be better than AddInt32 (#2077) 2022-07-01 12:41:32 +08:00
dependabot[bot]
ce7e2a2a9a chore(deps): bump github.com/pelletier/go-toml/v2 from 2.0.1 to 2.0.2 (#2072)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 23:27:53 +08:00
taobig
c92400ead2 fix 当表有唯一键时,update()的形参和实参不一致 (#2010) 2022-06-30 23:25:54 +08:00
dependabot[bot]
0b109c1954 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.4.1 to 4.4.2 (#2066)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.4.1...v4.4.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-30 00:21:08 +08:00
dependabot[bot]
d42979f705 chore(deps): bump github.com/stretchr/testify from 1.7.2 to 1.8.0 (#2068)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.2...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-29 22:08:01 +08:00
dependabot[bot]
29d81381c1 chore(deps): bump github.com/alicebob/miniredis/v2 from 2.21.0 to 2.22.0 (#2067)
Bumps [github.com/alicebob/miniredis/v2](https://github.com/alicebob/miniredis) from 2.21.0 to 2.22.0.
- [Release notes](https://github.com/alicebob/miniredis/releases)
- [Changelog](https://github.com/alicebob/miniredis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/alicebob/miniredis/compare/v2.21.0...v2.22.0)

---
updated-dependencies:
- dependency-name: github.com/alicebob/miniredis/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-29 21:53:02 +08:00
dependabot[bot]
89f6c97097 chore(deps): bump github.com/ClickHouse/clickhouse-go/v2 (#2064)
Bumps [github.com/ClickHouse/clickhouse-go/v2](https://github.com/ClickHouse/clickhouse-go) from 2.0.15 to 2.2.0.
- [Release notes](https://github.com/ClickHouse/clickhouse-go/releases)
- [Commits](https://github.com/ClickHouse/clickhouse-go/compare/v2.0.15...v2.2.0)

---
updated-dependencies:
- dependency-name: github.com/ClickHouse/clickhouse-go/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-29 21:52:07 +08:00
Kevin Wan
ff6f109065 Create dependabot.yml
add dependabot.
2022-06-29 21:38:47 +08:00
Zhang.Y
7da77302f4 fix: \u003cnil\u003e log output when http server shutdown. (#2055) 2022-06-29 21:35:01 +08:00
虫子樱桃
76086fc717 fix:typo in readme.md (#2061)
Co-authored-by: 虫子樱桃 <czyt@w.cn>
2022-06-29 19:38:23 +08:00
Kevin Wan
555c4ecd1a fix: quickstart wrong package when go.mod exists in parent dir (#2048)
* chore: fix typo

* fix: quickstart in dir with go.mod

* fix: runner failed

* chore: refine code

* chore: simplify quickstart mono
2022-06-26 22:37:15 +08:00
lord63
630dfa0887 [ci skip] Fix dead doc link (#2047) 2022-06-25 11:18:47 +08:00
Kevin Wan
38cd7b7df0 chore: remove lifecycle preStop because sh not exist in scratch (#2042) 2022-06-24 21:30:07 +08:00
Kevin Wan
9148f8df2a Update readme-cn.md 2022-06-24 20:22:35 +08:00
Kevin Wan
13f051d0e5 Update readme-cn.md 2022-06-22 22:01:00 +08:00
anqiansong
93b3f5030f chore: Add command desc & color commands (#2013)
* Add link & Color sub-commands

* Color sub-commands for unix-like OS

* Remove useless code

* Remove redundant dependency
2022-06-21 20:21:38 +08:00
anqiansong
b44e8f5c75 fix #1977 (#2034) 2022-06-21 20:01:42 +08:00
Kevin Wan
b9eb03e9a9 Update readme.md 2022-06-19 20:50:55 +08:00
Kevin Wan
86b531406b Update readme.md 2022-06-19 20:48:21 +08:00
Kevin Wan
47c49de94e feat: rest.WithChain to replace builtin middlewares (#2033)
* feat: rest.WithChain to replace builtin middlewares

* chore: add comments

* chore: refine code
2022-06-19 17:41:33 +08:00
Kevin Wan
50f16e2892 Update readme-cn.md 2022-06-19 14:22:29 +08:00
Kevin Wan
018ca82048 chore: refactor to simplify disabling builtin middlewares (#2031)
* chore: refactor to simplify disabling builtin middlewares

* chore: rename methods
2022-06-18 20:16:34 +08:00
magickeha
6976ba7e13 add user middleware chain function (#1913)
* add user middleware chain function

* fix staticcheck SA4006

* chang code Implementation style

Co-authored-by: kemq1 <kemq1@spdb.com.cn>
2022-06-18 18:45:47 +08:00
anqiansong
9b6e4c440c Add fig (#2008)
Co-authored-by: SH00414ml <sh00414ml@SH00414mldeMacBook-Pro.local>
2022-06-18 18:34:48 +08:00
Kevin Wan
9eea311a4d feat: support build Dockerfile from current dir (#2021) 2022-06-18 18:32:07 +08:00
chen quan
86d70317bf chore: upgrade action version (#2027) 2022-06-17 19:55:19 +08:00
chen quan
6518eb10b3 feat: add trace in httpc (#2011) 2022-06-17 15:01:14 +08:00
Kevin Wan
0147d7a9d1 Update readme-cn.md 2022-06-14 08:23:46 +08:00
Kevin Wan
1b2b7647d6 chore: coding style (#2012) 2022-06-14 07:25:54 +08:00
Atlan
af6d37c33d fix: 修复 clientinterceptors/tracinginterceptor.go 显示接受消息字节为0 (#2003) 2022-06-14 00:11:10 +08:00
Kevin Wan
3da5c5f530 Update readme.md 2022-06-13 19:39:59 +08:00
Kevin Wan
1694a92db0 Update readme.md 2022-06-13 19:35:51 +08:00
anqiansong
c27e00b45c feat: Replace mongo package with monc & mon (#2002)
* Replace mongo package with monc & mon

* Add terminal whitespace

* format code
2022-06-12 23:02:34 +08:00
Kevin Wan
ed1c937998 feat: convert grpc errors to http status codes (#1997)
* feat: convert grpc errors to http status codes

* chore: circuit break include unimplemented grpc error

* chore: add reference link in comments
2022-06-11 23:07:26 +08:00
Kevin Wan
db9a1f3e27 chore: rename methods (#1998) 2022-06-11 12:34:10 +08:00
马守越
392a390a3f periodlimit new function TakeWithContext (#1983)
Co-authored-by: mashouyue's m1max <mashouyue@toowow.cn>
2022-06-11 12:07:57 +08:00
Gaffey
2a900e1795 typo: add type keyword (#1992) 2022-06-11 11:46:50 +08:00
swliao425
0f5d8c6be3 feat: add 'imagePullPolicy' parameter for 'goctl kube deploy' (#1996) 2022-06-11 09:49:14 +08:00
MarkJoyMa
f2caf9237a fix goctl api clone template fail (#1990) 2022-06-09 23:35:03 +08:00
Kevin Wan
2f0e4e3ebf chore: update dependencies (#1985) 2022-06-09 23:34:06 +08:00
Kevin Wan
2c6b422f6b chore: update goctl version to 1.3.8 (#1981) 2022-06-06 20:07:39 +08:00
Kevin Wan
4d34998338 fix: generate bad Dockerfile on given dir (#1980) 2022-06-06 19:50:54 +08:00
anqiansong
8be47b9c99 Fix pg subcommand level error (#1979) 2022-06-06 19:42:31 +08:00
Kevin Wan
1d95e95cf8 chore: make methods consistent in signatures (#1971)
* chore: make methods consistent in signatures

* test: fix fails
2022-06-05 12:56:13 +08:00
taobig
3fa8c5940d fix: The validation of tag "options" is not working with int/uint type (#1969) 2022-06-05 11:51:43 +08:00
Kevin Wan
c44edd7cac test: fix fails (#1970) 2022-06-05 11:51:21 +08:00
Kevin Wan
af05219b70 test: make tests stable (#1968)
* test: make tests stable

* test: fix fails
2022-06-04 23:46:29 +08:00
Kevin Wan
f366e1d936 chore: make print pretty (#1967) 2022-06-04 19:53:22 +08:00
Kevin Wan
6c94e4652e chore: better mongo logs (#1965)
* chore: better mongo logs

* chore: add comments
2022-06-04 16:11:31 +08:00
kevin
edfaa6d906 🐞 fix: fixed typo (#1916)
Co-authored-by: kevinzhang <kevinzhang@moonton.com>
2022-06-04 14:50:06 +08:00
Kevin Wan
b6b96d9dad feat: print routes (#1964)
* feat: print rest routes

* feat: print rest routes
2022-06-04 13:26:14 +08:00
Kevin Wan
87800419f5 chore: update dependencies (#1963) 2022-06-04 12:31:59 +08:00
Kevin Wan
50a5fb7715 Update readme-cn.md 2022-06-03 23:03:31 +08:00
Kevin Wan
aa8f07d064 Update readme.md 2022-06-03 23:02:54 +08:00
Kevin Wan
7868bdf660 Chore/goctl version (#1962)
* chore: update version to v1.3.7

* docs: update migrate versions

* chore: remove debug prints

* chore: remove debug prints
2022-06-03 20:46:21 +08:00
Kevin Wan
46078e716d chore: update version (#1961) 2022-06-03 20:08:29 +08:00
Kevin Wan
bb33a20bc8 Update readme-cn.md 2022-06-03 19:18:58 +08:00
Kevin Wan
5536473a08 Update readme.md 2022-06-03 19:18:07 +08:00
Kevin Wan
323b35ed2d Update readme.md
update docs.
2022-06-03 19:15:34 +08:00
Kevin Wan
30958a91f7 docs: add docs for logx (#1960) 2022-06-03 19:11:06 +08:00
Kevin Wan
b94b68a427 chore: refactoring mapping string to slice (#1959) 2022-06-03 10:49:22 +08:00
家福
07145b210e fix: panic on convert to string on fillSliceFromString() (#1951)
* Update unmarshaler.go

 fix: 修复fillSliceFromString()方法中mapValue 强转string后的panic 错误

* test: 增加单元测试

增加单元测试

* Update unmarshaler_test.go
2022-06-03 00:27:48 +08:00
Kevin Wan
321a20add6 chore: update roadmap (#1948) 2022-06-02 09:28:29 +08:00
kunyu
65098d4737 Delete duplicated crash recover logic. (#1950)
* Update statinterceptor.go

* Update statinterceptor_test.go
2022-06-01 22:53:05 +08:00
Kevin Wan
35425f6164 Update readme-cn.md 2022-06-01 12:34:13 +08:00
Kevin Wan
a0060ff81b Update readme-cn.md 2022-05-31 10:05:59 +08:00
Kevin Wan
289a325757 chore: refine docker for better compatible with package main (#1944)
* chore: refine docker for better compatible with package main

* chore: default to current dir on goctl docker command
2022-05-30 13:26:58 +08:00
Kevin Wan
3fbe0f87b7 Update readme-cn.md 2022-05-28 18:54:45 +08:00
Kevin Wan
ea98d210fd Update readme-cn.md 2022-05-28 14:40:44 +08:00
Kevin Wan
b9bc1fdcf8 Update readme.md 2022-05-28 14:39:25 +08:00
Kevin Wan
6dc570bcd7 Update readme-cn.md 2022-05-28 14:36:13 +08:00
Kevin Wan
e21997f0d7 Update readme.md 2022-05-28 14:31:07 +08:00
Kevin Wan
92c0b7c3c5 Update readme-cn.md 2022-05-27 18:45:32 +08:00
vic
6d3ed98744 优化代码 (#1939) 2022-05-27 18:36:18 +08:00
NoTryNoSuccess
fb519fa547 core/mr:a little optimization for collector initialization in ForEach function (#1937)
Co-authored-by: notrynosuccess <daihongshan@gmail.com>
2022-05-27 17:19:40 +08:00
chen quan
e9501c3fb3 chore(action): simplified release configuration (#1935) 2022-05-27 16:31:05 +08:00
chen quan
fd12659729 chore: add release action to auto build binaries (#1884)
* chore: add release action to auto build binaries

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* fix: test bugs

Signed-off-by: chenquan <chenquan.dev@gmail.com>
2022-05-25 23:42:24 +08:00
Kevin Wan
72ebbb9774 feat: update docker alpine package mirror (#1924)
* feat: update docker alpine package mirror

* chore: format code
2022-05-23 09:13:21 +08:00
anqiansong
f1fdd55b38 Support built-in shorthand flags (#1925) 2022-05-23 09:13:12 +08:00
anqiansong
58787746db fix: Useless delete cache logic in update (#1923)
* Fix bug: useless delete cache logic in update

* Format code
2022-05-23 09:12:06 +08:00
Kevin Wan
ca88b69d24 feat: set default connection idle time for grpc servers (#1922)
* feat: set default connection idle time for grpc servers

* feat: add grpc health check
2022-05-21 19:38:27 +08:00
Kevin Wan
6b1e15cab1 chore: update k8s.io/client-go for security reason, go is upgrade to 1.16 (#1912)
* chore: fix jwt dependency security issue

* chore: update clickhouse driver

* chore: fix a security issue

* chore: update dependencies
2022-05-21 14:34:01 +08:00
Kevin Wan
6f86e5bff8 Update readme-cn.md 2022-05-20 19:13:49 +08:00
Kevin Wan
3f492df74e Update readme-cn.md 2022-05-17 23:23:48 +08:00
anqiansong
5e7b1f6bfe Fix process blocking problem during check (#1911) 2022-05-17 09:42:18 +08:00
Kevin Wan
e80a64fa67 feat: support WithStreamClientInterceptor for zrpc clients (#1907)
* feat: support WithStreamClientInterceptor for zrpc clients

* fix: data race
2022-05-14 19:58:17 +08:00
Kevin Wan
95282edb78 Update FUNDING.yml
update sponsor
2022-05-14 17:29:26 +08:00
Kevin Wan
7b82eda993 chore: use get for quickstart, plain logs for easy understanding (#1905) 2022-05-14 17:01:37 +08:00
Kevin Wan
5d09cd0e7c use goproxy properly, remove files (#1903) 2022-05-14 16:00:20 +08:00
Kevin Wan
1e717f9f5c feat: add toml config (#1899) 2022-05-13 23:17:43 +08:00
Kevin Wan
c6e2b4a43a chore: coding style for quickstart (#1902) 2022-05-13 23:10:55 +08:00
chen quan
e567a0c718 refactor: refactor trace in redis & sql & mongo (#1865)
* refactor: refactor tracing in redis & sql & mongo

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* fix: fix some tests

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: add missing content

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: adjust `log` and `return`

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: reformat code

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: reformat code

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: reformat code

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: simpler span name

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: fix a bug

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* refactor: fix a bug

Signed-off-by: chenquan <chenquan.dev@gmail.com>
2022-05-13 12:32:34 +08:00
anqiansong
52f060caae feat: Add goctl quickstart (#1889)
* Add goctl quickstart

* Format code

* Format code
2022-05-13 12:23:24 +08:00
anqiansong
f486685e99 Fix code generation (#1897) 2022-05-13 00:16:17 +08:00
过客龙门
3ae874d75d fix ts tpl (#1879) 2022-05-11 23:45:32 +08:00
Kevin Wan
c58eb13328 Update readme-cn.md
update logo
2022-05-11 23:33:54 +08:00
givemeafish
14ca39bc86 fix:tools/goctl/rpc/generator/template_test.go file has wrong parameters (#1882) 2022-05-11 23:24:34 +08:00
Kevin Wan
3ea8a2d4b6 Update readme-cn.md 2022-05-11 18:19:00 +08:00
Kevin Wan
6d2b9fd904 chore: improve codecov (#1878) 2022-05-08 13:17:48 +08:00
Kevin Wan
5451d96a81 chore: update some logs (#1875) 2022-05-07 23:34:55 +08:00
Kevin Wan
69c2bad410 feat: logx with color (#1872)
* feat: logx with color

* chore: update logs

* fix test error

* chore: change colors of http codes

* chore: add comments

* chore: use faith/color instead of ascii code color

* chore: update colors

* chore: update colors

* chore: fix duplicated slowcall text

* chore: remove slowcall colors
2022-05-07 23:22:39 +08:00
anqiansong
5383e29ce6 feat: Replace cli to cobra (#1855)
* Replace cli

* Replace cli

* Replace cli

* Format code

* Add compare case

* Add compare case

* Add compare case

* Support go style flag

* Support go style flag

* Add test case
2022-05-07 15:40:11 +08:00
Kevin Wan
51472004a3 Update readme.md 2022-05-07 10:11:21 +08:00
Kevin Wan
caf5b7b1f1 Update readme-cn.md 2022-05-07 10:10:44 +08:00
Kevin Wan
bef9aa55e6 Update readme.md 2022-05-07 10:08:25 +08:00
Kevin Wan
d0a59b13a6 chore: fix deprecated usages (#1871)
* add conf documents

* chore: use {} instead of () for environment variables

* chore: fix deprecated usages

* chore: fix unstable tests

* chore: show stack on github actions
2022-05-06 15:13:46 +08:00
Kevin Wan
469e62067c add conf documents (#1869)
* add conf documents

* chore: use {} instead of () for environment variables
2022-05-06 11:05:06 +08:00
Kevin Wan
a36d58aac9 fix time, duration, slice types on logx.Field (#1868)
* chore: refine tests

* fix #1866
2022-05-05 23:37:18 +08:00
Kevin Wan
aa5118c2aa chore: refine tests (#1864) 2022-05-04 17:52:58 +08:00
Kevin Wan
974ba5c9aa test: add codecov (#1863) 2022-05-04 16:19:51 +08:00
Kevin Wan
ec1de4f48d test: add codecov (#1861)
* test: add codecov

* test: add codecov
2022-05-03 21:22:15 +08:00
Kevin Wan
bab72b7630 chore: use time.Now() instead of timex.Time() because go optimized it (#1860) 2022-05-03 19:51:47 +08:00
Kevin Wan
ac321fc146 feat: add fields with logx methods, support using third party logging libs. (#1847)
* backup

* simplify

* chore: remove unused pool

* chore: fix lint errors

* chore: use strings.Builder instead of bytes.Buffer

* test: add more tests

* chore: fix reviewdog

* test: fix data race

* feat: make logger customizable

* chore: fix reviewdog

* test: fix fails

* chore: fix set writer twice

* chore: use context instead of golang.org context

* chore: specify uint32 for level types
2022-05-03 17:34:26 +08:00
全自动盒子
ae2c76765c fix typo (#1857) 2022-05-03 16:25:13 +08:00
Kevin Wan
f21970c117 test: add more tests (#1856) 2022-05-02 21:24:20 +08:00
Kevin Wan
d0a58d1f2d docs: update readme (#1849) 2022-05-01 12:48:47 +08:00
Kevin Wan
3bbc90ec24 refactor: move json related header vars to internal (#1840)
* refactor: move json related header vars to internal

* refactor: use header.ContentType
2022-04-28 15:12:04 +08:00
Kevin Wan
cef83efd4e fix #1838 (#1839) 2022-04-28 11:25:26 +08:00
anqiansong
cc09ab2aba feat: Support model code generation for multi tables (#1836)
* Support model code generation for multi tables

* Format code

* Format code

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2022-04-28 10:01:04 +08:00
Kevin Wan
f7a60cdc24 fix: remove deprecated dependencies (#1837)
* fix: remove deprecated dependencies

* backup

* fix test error
2022-04-27 21:34:54 +08:00
Kevin Wan
c3a49ece8d Update readme-cn.md
add go-zero users.
2022-04-27 13:50:54 +08:00
Kevin Wan
1a38eddffe refactor: simplify the code (#1835) 2022-04-27 10:44:24 +08:00
Kevin Wan
5bcee4cf7c fix #1806 (#1833)
* fix #1806

* chore: refine error text
2022-04-27 00:01:31 +08:00
Kevin Wan
5c9fae7e62 feat: support sub domain for cors (#1827) 2022-04-25 21:56:59 +08:00
Kevin Wan
ec3e02624c feat: upgrade grpc to 1.46, and remove the deprecated grpc.WithBalancerName (#1820) 2022-04-24 22:42:40 +08:00
chen quan
22b157bb6c chore: optimize code (#1818)
Signed-off-by: chenquan <chenquan.dev@gmail.com>
2022-04-23 22:02:04 +08:00
Kevin Wan
095b603788 chore: remove gofumpt -s flag, default to be enabled (#1816) 2022-04-22 14:37:17 +08:00
Kevin Wan
bc3c9484d1 chore: refactor (#1814) 2022-04-22 09:37:09 +08:00
chen quan
162e9cef86 feat: add trace in redis & mon & sql (#1799)
* feat: add sub spanId with redis

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* add tests

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* fix a bug

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* feat: add sub spanId in sql

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* feat: add sub spanId in mon

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* chore: optimize code

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* feat: add breaker in warpSession

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* chore: optimize code

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* test: add tests

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* chore: reformat code

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* fix: fix typo

Signed-off-by: chenquan <chenquan.dev@gmail.com>

* fix a bug

Signed-off-by: chenquan <chenquan.dev@gmail.com>
2022-04-22 09:04:44 +08:00
Vee Zhang
94ddb3380e fix: rest: WriteJson get 200 when Marshal failed. (#1803)
Only the first WriteHeader call takes effect.
2022-04-21 21:55:01 +08:00
anqiansong
16c61c6657 chore: Embed unit test data (#1812)
* Embed unit test data

* Add testdata

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2022-04-21 21:49:09 +08:00
chowyu12
14bf2f33f7 add go-grpc_opt and go_opt for grpc new command (#1769)
Co-authored-by: zhouyy <zhouyy@ickey.cn>
2022-04-21 16:45:56 +08:00
anqiansong
305587aa81 fix: Fix issue #1810 (#1811)
* Fix #1810

* Remove go embed

* Format code

* Remove useless code

Co-authored-by: anqiansong <anqiansong@bytedance.com>
2022-04-21 15:22:43 +08:00
Kevin Wan
2cdff97934 feat: use mongodb official driver instead of mgo (#1782)
* wip: backup

* wip: backup

* wip: backup

* backup

* backup

* backup

* add more tests

* fix wrong dependency

* fix lint errors

* remove test due to data race

* add tests

* fix test error

* add mon.Model

* add mon.Model unmarshal

* add monc

* add more tests for monc

* add more tests for monc

* add docs for mon and monc packages

* fix doc errors

* chhore: add comment

* chore: fix test bug

* chore: refine tests

* chore: remove primitive.NewObjectID in test code

* chore: rename test files for typo
2022-04-19 14:03:04 +08:00
fang duan
bbe1249ecb update rpc generate sample proto file (#1709)
* update rpc generate sample proto file

* update
2022-04-19 10:59:16 +08:00
Fyn
e62870e268 feat(goctl): go work multi-module support (#1800)
* feat(goctl): go work multi-module support

Resolve: #1793

* chore: print log when getting project ctx fails
2022-04-18 20:36:41 +08:00
Kevin Wan
92b450eb11 fix: ignore timeout on websocket (#1802) 2022-04-18 20:14:46 +08:00
杨圆建
d58cf7a12a fix: Hdel check result & Pfadd check result (#1801) 2022-04-18 17:38:36 +08:00
Fyn
036d803fbb docs(goctl): goctl 1.3.4 migration note (#1780)
* docs(goctl): goctl 1.3.4 migration note

* adds a simple lang check
* adds migration notes

* chore: remove i18n

* chore: remove todo
2022-04-18 14:42:13 +08:00
chen quan
c6ab11b14f chore: use grpc.WithTransportCredentials and insecure.NewCredentials() instead of grpc.WithInsecure (#1798)
Signed-off-by: chenquan <chenquan.dev@gmail.com>
2022-04-18 14:15:09 +08:00
Kevin Wan
9e20b1bbfe chhore: fix usage typo (#1797) 2022-04-17 21:17:31 +08:00
fang duan
fadef0ccd9 goctl api new should given a service_name explictly (#1688) 2022-04-17 20:59:18 +08:00
fang duan
4382ec0e0d show help when running goctl api without any flags (#1678)
close #1676
2022-04-17 20:58:12 +08:00
fang duan
db99addc64 show help when running goctl docker without any flags (#1679)
close #1677
2022-04-17 20:57:46 +08:00
fang duan
97bf3856c1 show help when running goctl rpc protoc without any flags (#1683) 2022-04-17 20:57:26 +08:00
fang duan
ff6c6558dd improve goctl rpc new (#1687) 2022-04-17 20:56:56 +08:00
Kevin Wan
5d4e7c84ee revert postgres package refactor (#1796)
* Revert "refactor: move postgres to pg package (#1781)"

This reverts commit ba8ac974aa.

* remove pg, use postgres
2022-04-17 12:07:48 +08:00
Kevin Wan
cb4fcf2c6c fix marshal ptr in httpc (#1789)
* fix marshal ptr in httpc

* add more tests

* add more tests

* add more tests

* fix issue on options and optional both provided
2022-04-15 19:07:34 +08:00
Fyn
ee88abce14 fix(goctl): api/new/api.tpl (#1788) 2022-04-14 23:43:48 +08:00
Kevin Wan
ecc3653d44 fix #1729 (#1783) 2022-04-13 19:06:00 +08:00
Kevin Wan
ba8ac974aa refactor: move postgres to pg package (#1781) 2022-04-13 12:46:09 +08:00
Kevin Wan
50de01fb49 feat: add httpc.Do & httpc.Service.Do (#1775)
* backup

* backup

* backup

* feat: add httpc.Do & httpc.Service.Do

* fix: not using strings.Cut, it's from Go 1.18

* chore: remove redudant code

* feat: httpc.Do finished

* chore: fix reviewdog

* chore: break loop if found

* add more tests
2022-04-11 11:00:28 +08:00
方航
fabea4c448 fix bug: crash when generate model with goctl. (#1777)
* fix bug: crash when generate model with goctl.

situation: column name with line.

CREATE TABLE test (
id int NOT NULL AUTO_INCREMENT,
zh-cn text CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '中文简体',
PRIMARY KEY (id) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

* group imports

group imports

* Use

go-zero/tools/goctl/util/string.go
 func SafeString(in string) string {
instead of ReplaceAll

Co-authored-by: 方航 <fanghang@tange.ai>
2022-04-11 10:11:40 +08:00
Fyn
6d9dfc08f9 feat(goctl): supports api multi-level importing (#1747)
* feat(goctl): supports api  multi-level importing

Resolves: #1744

* fix(goctl): import-cycle, etc.

import-cycle will not be allowed
e.g., a.api -> b.api -> a.api
regular multiple-import will be allowed
e.g., a.api -> b.api -> c.api
                   -> c.api

* refactor(goctl): adds comments to exported var

* fix(goctl): typo in a comment
2022-04-09 23:26:57 +08:00
anqiansong
252fabcc4b fix nil pointer if group not exists (#1773)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2022-04-09 11:54:30 +08:00
Kevin Wan
415c4c91fc fix: model unique keys generated differently in each re-generation (#1771) 2022-04-09 00:25:23 +08:00
fang duan
0cc9d4ff8d show help when running goctl rpc template without any flags (#1685)
close #1684
2022-04-08 22:28:45 +08:00
Kevin Wan
8bc34defc4 chore: avoid deadlock after stopping TimingWheel (#1768) 2022-04-07 11:50:18 +08:00
anqiansong
8dd764679c Fix #1765 (#1767)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2022-04-07 10:40:21 +08:00
Kevin Wan
9fe868ade9 chore: remove legacy code (#1766) 2022-04-06 23:24:20 +08:00
Kevin Wan
4e48286838 chore: add doc (#1764) 2022-04-06 22:42:40 +08:00
Kevin Wan
ab01442d46 add more tests (#1763)
* feat: add goctl docker build scripts

* chore: add more tests
2022-04-06 16:09:06 +08:00
Kevin Wan
8694e38384 feat: add goctl docker build scripts (#1760) 2022-04-05 13:07:05 +08:00
Kevin Wan
d5e550e79b Update readme-cn.md 2022-04-05 11:51:53 +08:00
Kevin Wan
affdab660e Update readme.md 2022-04-05 11:51:09 +08:00
Kevin Wan
7d5858e83a Update readme.md 2022-04-05 11:08:00 +08:00
Kevin Wan
815a6a6485 Update readme-cn.md 2022-04-05 11:07:37 +08:00
benqi
475d17e17d feat: support ctx in kv methods (#1759) 2022-04-04 23:19:58 +08:00
Kevin Wan
8472415472 fix #1754 (#1757) 2022-04-04 22:13:08 +08:00
Kevin Wan
faad6e27e3 feat: use go:embed to embed templates (#1756) 2022-04-04 13:12:05 +08:00
anqiansong
58a0b17451 Support goctl env install (#1752)
Co-authored-by: anqiansong <anqiansong@bytedance.com>
2022-04-03 21:58:43 +08:00
Kevin Wan
89eccfdb97 chore: update go-zero to v1.3.2 in goctl (#1750) 2022-04-03 20:44:33 +08:00
495 changed files with 22429 additions and 7888 deletions

3
.github/FUNDING.yml vendored
View File

@@ -9,4 +9,5 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
custom: https://gitee.com/kevwan/static/raw/master/images/sponsor.jpg custom: # https://gitee.com/kevwan/static/raw/master/images/sponsor.jpg
ethereum: 0x5052b7f6B937B02563996D23feb69b38D06Ca150 | kevwan

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View File

@@ -35,11 +35,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View File

@@ -14,11 +14,11 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.15 go-version: ^1.16
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Get dependencies - name: Get dependencies
run: | run: |
@@ -28,7 +28,7 @@ jobs:
run: | run: |
go vet -stdmethods=false $(go list ./...) go vet -stdmethods=false $(go list ./...)
go install mvdan.cc/gofumpt@latest go install mvdan.cc/gofumpt@latest
test -z "$(gofumpt -s -l -extra .)" || echo "Please run 'gofumpt -l -w -extra .'" test -z "$(gofumpt -l -extra .)" || echo "Please run 'gofumpt -l -w -extra .'"
- name: Test - name: Test
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
@@ -43,10 +43,10 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.15 go-version: ^1.16
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Test - name: Test
run: | run: |

View File

@@ -9,8 +9,8 @@ jobs:
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v3
with: with:
days-before-issue-stale: 30 days-before-issue-stale: 365
days-before-issue-close: 14 days-before-issue-close: 90
stale-issue-label: "stale" stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."

28
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
on:
push:
tags:
- "tools/goctl/*"
jobs:
releases-matrix:
name: Release goctl binary
runs-on: ubuntu-latest
strategy:
matrix:
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
goos: [ linux, windows, darwin ]
goarch: [ "386", amd64, arm64 ]
exclude:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v3
- uses: zeromicro/go-zero-release-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://dl.google.com/go/go1.17.5.linux-amd64.tar.gz"
project_path: "tools/goctl"
binary_name: "goctl"
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md

View File

@@ -5,7 +5,7 @@ jobs:
name: runner / staticcheck name: runner / staticcheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: reviewdog/action-staticcheck@v1 - uses: reviewdog/action-staticcheck@v1
with: with:
github_token: ${{ secrets.github_token }} github_token: ${{ secrets.github_token }}

3
.gitignore vendored
View File

@@ -17,7 +17,8 @@
# for test purpose # for test purpose
**/adhoc **/adhoc
**/testdata go.work
go.work.sum
# gitlab ci # gitlab ci
.cache .cache

View File

@@ -22,7 +22,7 @@ We hope that the items listed below will inspire further engagement from the com
## 2022 ## 2022
- [x] Support `context` in redis related methods for timeout and tracing - [x] Support `context` in redis related methods for timeout and tracing
- [x] Support `context` in sql related methods for timeout and tracing - [x] Support `context` in sql related methods for timeout and tracing
- [ ] Support `context` in mongodb related methods for timeout and tracing - [x] Support `context` in mongodb related methods for timeout and tracing
- [x] Add `httpc.Do` with HTTP call governance, like circuit breaker etc. - [x] Add `httpc.Do` with HTTP call governance, like circuit breaker etc.
- [ ] Support `goctl doctor` command to report potential issues for given service - [ ] Support `goctl doctor` command to report potential issues for given service
- [ ] Support `goctl mock` command to start a mocking server with given `.api` file - [ ] Support `goctl mock` command to start a mocking server with given `.api` file

View File

@@ -69,11 +69,8 @@ func (f *Filter) Exists(data []byte) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
if !isSet {
return false, nil
}
return true, nil return isSet, nil
} }
func (f *Filter) getLocations(data []byte) []uint { func (f *Filter) getLocations(data []byte) []uint {

View File

@@ -5,12 +5,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"time"
"github.com/zeromicro/go-zero/core/mathx" "github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/proc" "github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/stat" "github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/stringx" "github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/core/timex"
) )
const ( const (
@@ -198,7 +198,7 @@ type errorWindow struct {
func (ew *errorWindow) add(reason string) { func (ew *errorWindow) add(reason string) {
ew.lock.Lock() ew.lock.Lock()
ew.reasons[ew.index] = fmt.Sprintf("%s %s", timex.Time().Format(timeFormat), reason) ew.reasons[ew.index] = fmt.Sprintf("%s %s", time.Now().Format(timeFormat), reason)
ew.index = (ew.index + 1) % numHistoryReasons ew.index = (ew.index + 1) % numHistoryReasons
ew.count = mathx.MinInt(ew.count+1, numHistoryReasons) ew.count = mathx.MinInt(ew.count+1, numHistoryReasons)
ew.lock.Unlock() ew.lock.Unlock()

View File

@@ -7,7 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/pem" "encoding/pem"
"errors" "errors"
"io/ioutil" "os"
) )
var ( var (
@@ -48,7 +48,7 @@ type (
// NewRsaDecrypter returns a RsaDecrypter with the given file. // NewRsaDecrypter returns a RsaDecrypter with the given file.
func NewRsaDecrypter(file string) (RsaDecrypter, error) { func NewRsaDecrypter(file string) (RsaDecrypter, error) {
content, err := ioutil.ReadFile(file) content, err := os.ReadFile(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -26,7 +26,7 @@ type (
// CacheOption defines the method to customize a Cache. // CacheOption defines the method to customize a Cache.
CacheOption func(cache *Cache) CacheOption func(cache *Cache)
// A Cache object is a in-memory cache. // A Cache object is an in-memory cache.
Cache struct { Cache struct {
name string name string
lock sync.Mutex lock sync.Mutex
@@ -98,13 +98,18 @@ func (c *Cache) Get(key string) (interface{}, bool) {
// Set sets value into c with key. // Set sets value into c with key.
func (c *Cache) Set(key string, value interface{}) { func (c *Cache) Set(key string, value interface{}) {
c.SetWithExpire(key, value, c.expire)
}
// SetWithExpire sets value into c with key and expire with the given value.
func (c *Cache) SetWithExpire(key string, value interface{}, expire time.Duration) {
c.lock.Lock() c.lock.Lock()
_, ok := c.data[key] _, ok := c.data[key]
c.data[key] = value c.data[key] = value
c.lruCache.add(key) c.lruCache.add(key)
c.lock.Unlock() c.lock.Unlock()
expiry := c.unstableExpiry.AroundDuration(c.expire) expiry := c.unstableExpiry.AroundDuration(expire)
if ok { if ok {
c.timingWheel.MoveTimer(key, expiry) c.timingWheel.MoveTimer(key, expiry)
} else { } else {

View File

@@ -18,7 +18,7 @@ func TestCacheSet(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
cache.Set("first", "first element") cache.Set("first", "first element")
cache.Set("second", "second element") cache.SetWithExpire("second", "second element", time.Second*3)
value, ok := cache.Get("first") value, ok := cache.Get("first")
assert.True(t, ok) assert.True(t, ok)

View File

@@ -68,6 +68,24 @@ func (m *SafeMap) Get(key interface{}) (interface{}, bool) {
return val, ok return val, ok
} }
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
func (m *SafeMap) Range(f func(key, val interface{}) bool) {
m.lock.RLock()
defer m.lock.RUnlock()
for k, v := range m.dirtyOld {
if !f(k, v) {
return
}
}
for k, v := range m.dirtyNew {
if !f(k, v) {
return
}
}
}
// Set sets the value into m with the given key. // Set sets the value into m with the given key.
func (m *SafeMap) Set(key, value interface{}) { func (m *SafeMap) Set(key, value interface{}) {
m.lock.Lock() m.lock.Lock()

View File

@@ -1,6 +1,7 @@
package collection package collection
import ( import (
"sync/atomic"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -107,3 +108,42 @@ func testSafeMapWithParameters(t *testing.T, size, exception int) {
} }
} }
} }
func TestSafeMap_Range(t *testing.T) {
const (
size = 100000
exception1 = 5
exception2 = 500
)
m := NewSafeMap()
newMap := NewSafeMap()
for i := 0; i < size; i++ {
m.Set(i, i)
}
for i := 0; i < size; i++ {
if i%exception1 == 0 {
m.Del(i)
}
}
for i := size; i < size<<1; i++ {
m.Set(i, i)
}
for i := size; i < size<<1; i++ {
if i%exception2 != 0 {
m.Del(i)
}
}
var count int32
m.Range(func(k, v interface{}) bool {
atomic.AddInt32(&count, 1)
newMap.Set(k, v)
return true
})
assert.Equal(t, int(atomic.LoadInt32(&count)), m.Size())
assert.Equal(t, m.dirtyNew, newMap.dirtyNew)
assert.Equal(t, m.dirtyOld, newMap.dirtyOld)
}

View File

@@ -2,6 +2,7 @@ package collection
import ( import (
"container/list" "container/list"
"errors"
"fmt" "fmt"
"time" "time"
@@ -12,6 +13,11 @@ import (
const drainWorkers = 8 const drainWorkers = 8
var (
ErrClosed = errors.New("TimingWheel is closed already")
ErrArgument = errors.New("incorrect task argument")
)
type ( type (
// Execute defines the method to execute the task. // Execute defines the method to execute the task.
Execute func(key, value interface{}) Execute func(key, value interface{})
@@ -59,14 +65,15 @@ type (
// NewTimingWheel returns a TimingWheel. // NewTimingWheel returns a TimingWheel.
func NewTimingWheel(interval time.Duration, numSlots int, execute Execute) (*TimingWheel, error) { func NewTimingWheel(interval time.Duration, numSlots int, execute Execute) (*TimingWheel, error) {
if interval <= 0 || numSlots <= 0 || execute == nil { if interval <= 0 || numSlots <= 0 || execute == nil {
return nil, fmt.Errorf("interval: %v, slots: %d, execute: %p", interval, numSlots, execute) return nil, fmt.Errorf("interval: %v, slots: %d, execute: %p",
interval, numSlots, execute)
} }
return newTimingWheelWithClock(interval, numSlots, execute, timex.NewTicker(interval)) return newTimingWheelWithClock(interval, numSlots, execute, timex.NewTicker(interval))
} }
func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execute, ticker timex.Ticker) ( func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execute,
*TimingWheel, error) { ticker timex.Ticker) (*TimingWheel, error) {
tw := &TimingWheel{ tw := &TimingWheel{
interval: interval, interval: interval,
ticker: ticker, ticker: ticker,
@@ -89,47 +96,67 @@ func newTimingWheelWithClock(interval time.Duration, numSlots int, execute Execu
} }
// Drain drains all items and executes them. // Drain drains all items and executes them.
func (tw *TimingWheel) Drain(fn func(key, value interface{})) { func (tw *TimingWheel) Drain(fn func(key, value interface{})) error {
tw.drainChannel <- fn select {
case tw.drainChannel <- fn:
return nil
case <-tw.stopChannel:
return ErrClosed
}
} }
// MoveTimer moves the task with the given key to the given delay. // MoveTimer moves the task with the given key to the given delay.
func (tw *TimingWheel) MoveTimer(key interface{}, delay time.Duration) { func (tw *TimingWheel) MoveTimer(key interface{}, delay time.Duration) error {
if delay <= 0 || key == nil { if delay <= 0 || key == nil {
return return ErrArgument
} }
tw.moveChannel <- baseEntry{ select {
case tw.moveChannel <- baseEntry{
delay: delay, delay: delay,
key: key, key: key,
}:
return nil
case <-tw.stopChannel:
return ErrClosed
} }
} }
// RemoveTimer removes the task with the given key. // RemoveTimer removes the task with the given key.
func (tw *TimingWheel) RemoveTimer(key interface{}) { func (tw *TimingWheel) RemoveTimer(key interface{}) error {
if key == nil { if key == nil {
return return ErrArgument
} }
tw.removeChannel <- key select {
case tw.removeChannel <- key:
return nil
case <-tw.stopChannel:
return ErrClosed
}
} }
// SetTimer sets the task value with the given key to the delay. // SetTimer sets the task value with the given key to the delay.
func (tw *TimingWheel) SetTimer(key, value interface{}, delay time.Duration) { func (tw *TimingWheel) SetTimer(key, value interface{}, delay time.Duration) error {
if delay <= 0 || key == nil { if delay <= 0 || key == nil {
return return ErrArgument
} }
tw.setChannel <- timingEntry{ select {
case tw.setChannel <- timingEntry{
baseEntry: baseEntry{ baseEntry: baseEntry{
delay: delay, delay: delay,
key: key, key: key,
}, },
value: value, value: value,
}:
return nil
case <-tw.stopChannel:
return ErrClosed
} }
} }
// Stop stops tw. // Stop stops tw. No more actions after stopping a TimingWheel.
func (tw *TimingWheel) Stop() { func (tw *TimingWheel) Stop() {
close(tw.stopChannel) close(tw.stopChannel)
} }

View File

@@ -28,7 +28,6 @@ func TestTimingWheel_Drain(t *testing.T) {
ticker := timex.NewFakeTicker() ticker := timex.NewFakeTicker()
tw, _ := newTimingWheelWithClock(testStep, 10, func(k, v interface{}) { tw, _ := newTimingWheelWithClock(testStep, 10, func(k, v interface{}) {
}, ticker) }, ticker)
defer tw.Stop()
tw.SetTimer("first", 3, testStep*4) tw.SetTimer("first", 3, testStep*4)
tw.SetTimer("second", 5, testStep*7) tw.SetTimer("second", 5, testStep*7)
tw.SetTimer("third", 7, testStep*7) tw.SetTimer("third", 7, testStep*7)
@@ -56,6 +55,8 @@ func TestTimingWheel_Drain(t *testing.T) {
}) })
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
assert.Equal(t, 0, count) assert.Equal(t, 0, count)
tw.Stop()
assert.Equal(t, ErrClosed, tw.Drain(func(key, value interface{}) {}))
} }
func TestTimingWheel_SetTimerSoon(t *testing.T) { func TestTimingWheel_SetTimerSoon(t *testing.T) {
@@ -102,6 +103,13 @@ func TestTimingWheel_SetTimerWrongDelay(t *testing.T) {
}) })
} }
func TestTimingWheel_SetTimerAfterClose(t *testing.T) {
ticker := timex.NewFakeTicker()
tw, _ := newTimingWheelWithClock(testStep, 10, func(k, v interface{}) {}, ticker)
tw.Stop()
assert.Equal(t, ErrClosed, tw.SetTimer("any", 3, testStep))
}
func TestTimingWheel_MoveTimer(t *testing.T) { func TestTimingWheel_MoveTimer(t *testing.T) {
run := syncx.NewAtomicBool() run := syncx.NewAtomicBool()
ticker := timex.NewFakeTicker() ticker := timex.NewFakeTicker()
@@ -111,7 +119,6 @@ func TestTimingWheel_MoveTimer(t *testing.T) {
assert.Equal(t, 3, v.(int)) assert.Equal(t, 3, v.(int))
ticker.Done() ticker.Done()
}, ticker) }, ticker)
defer tw.Stop()
tw.SetTimer("any", 3, testStep*4) tw.SetTimer("any", 3, testStep*4)
tw.MoveTimer("any", testStep*7) tw.MoveTimer("any", testStep*7)
tw.MoveTimer("any", -testStep) tw.MoveTimer("any", -testStep)
@@ -125,6 +132,8 @@ func TestTimingWheel_MoveTimer(t *testing.T) {
} }
assert.Nil(t, ticker.Wait(waitTime)) assert.Nil(t, ticker.Wait(waitTime))
assert.True(t, run.True()) assert.True(t, run.True())
tw.Stop()
assert.Equal(t, ErrClosed, tw.MoveTimer("any", time.Millisecond))
} }
func TestTimingWheel_MoveTimerSoon(t *testing.T) { func TestTimingWheel_MoveTimerSoon(t *testing.T) {
@@ -175,6 +184,7 @@ func TestTimingWheel_RemoveTimer(t *testing.T) {
ticker.Tick() ticker.Tick()
} }
tw.Stop() tw.Stop()
assert.Equal(t, ErrClosed, tw.RemoveTimer("any"))
} }
func TestTimingWheel_SetTimer(t *testing.T) { func TestTimingWheel_SetTimer(t *testing.T) {

73
core/color/color.go Normal file
View File

@@ -0,0 +1,73 @@
package color
import "github.com/fatih/color"
const (
// NoColor is no color for both foreground and background.
NoColor Color = iota
// FgBlack is the foreground color black.
FgBlack
// FgRed is the foreground color red.
FgRed
// FgGreen is the foreground color green.
FgGreen
// FgYellow is the foreground color yellow.
FgYellow
// FgBlue is the foreground color blue.
FgBlue
// FgMagenta is the foreground color magenta.
FgMagenta
// FgCyan is the foreground color cyan.
FgCyan
// FgWhite is the foreground color white.
FgWhite
// BgBlack is the background color black.
BgBlack
// BgRed is the background color red.
BgRed
// BgGreen is the background color green.
BgGreen
// BgYellow is the background color yellow.
BgYellow
// BgBlue is the background color blue.
BgBlue
// BgMagenta is the background color magenta.
BgMagenta
// BgCyan is the background color cyan.
BgCyan
// BgWhite is the background color white.
BgWhite
)
var colors = map[Color][]color.Attribute{
FgBlack: {color.FgBlack, color.Bold},
FgRed: {color.FgRed, color.Bold},
FgGreen: {color.FgGreen, color.Bold},
FgYellow: {color.FgYellow, color.Bold},
FgBlue: {color.FgBlue, color.Bold},
FgMagenta: {color.FgMagenta, color.Bold},
FgCyan: {color.FgCyan, color.Bold},
FgWhite: {color.FgWhite, color.Bold},
BgBlack: {color.BgBlack, color.FgHiWhite, color.Bold},
BgRed: {color.BgRed, color.FgHiWhite, color.Bold},
BgGreen: {color.BgGreen, color.FgHiWhite, color.Bold},
BgYellow: {color.BgHiYellow, color.FgHiBlack, color.Bold},
BgBlue: {color.BgBlue, color.FgHiWhite, color.Bold},
BgMagenta: {color.BgMagenta, color.FgHiWhite, color.Bold},
BgCyan: {color.BgCyan, color.FgHiWhite, color.Bold},
BgWhite: {color.BgHiWhite, color.FgHiBlack, color.Bold},
}
type Color uint32
// WithColor returns a string with the given color applied.
func WithColor(text string, colour Color) string {
c := color.New(colors[colour]...)
return c.Sprint(text)
}
// WithColorPadding returns a string with the given color applied with leading and trailing spaces.
func WithColorPadding(text string, colour Color) string {
return WithColor(" "+text+" ", colour)
}

17
core/color/color_test.go Normal file
View File

@@ -0,0 +1,17 @@
package color
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithColor(t *testing.T) {
output := WithColor("Hello", BgRed)
assert.Equal(t, "Hello", output)
}
func TestWithColorPadding(t *testing.T) {
output := WithColorPadding("Hello", BgRed)
assert.Equal(t, " Hello ", output)
}

View File

@@ -2,28 +2,29 @@ package conf
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"path" "path"
"strings"
"github.com/zeromicro/go-zero/core/mapping" "github.com/zeromicro/go-zero/core/mapping"
) )
var loaders = map[string]func([]byte, interface{}) error{ var loaders = map[string]func([]byte, interface{}) error{
".json": LoadConfigFromJsonBytes, ".json": LoadFromJsonBytes,
".yaml": LoadConfigFromYamlBytes, ".toml": LoadFromTomlBytes,
".yml": LoadConfigFromYamlBytes, ".yaml": LoadFromYamlBytes,
".yml": LoadFromYamlBytes,
} }
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable. // Load loads config into v from file, .json, .yaml and .yml are acceptable.
func LoadConfig(file string, v interface{}, opts ...Option) error { func Load(file string, v interface{}, opts ...Option) error {
content, err := ioutil.ReadFile(file) content, err := os.ReadFile(file)
if err != nil { if err != nil {
return err return err
} }
loader, ok := loaders[path.Ext(file)] loader, ok := loaders[strings.ToLower(path.Ext(file))]
if !ok { if !ok {
return fmt.Errorf("unrecognized file type: %s", file) return fmt.Errorf("unrecognized file type: %s", file)
} }
@@ -40,19 +41,42 @@ func LoadConfig(file string, v interface{}, opts ...Option) error {
return loader(content, v) return loader(content, v)
} }
// LoadConfigFromJsonBytes loads config into v from content json bytes. // LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
func LoadConfigFromJsonBytes(content []byte, v interface{}) error { // Deprecated: use Load instead.
func LoadConfig(file string, v interface{}, opts ...Option) error {
return Load(file, v, opts...)
}
// LoadFromJsonBytes loads config into v from content json bytes.
func LoadFromJsonBytes(content []byte, v interface{}) error {
return mapping.UnmarshalJsonBytes(content, v) return mapping.UnmarshalJsonBytes(content, v)
} }
// LoadConfigFromYamlBytes loads config into v from content yaml bytes. // LoadConfigFromJsonBytes loads config into v from content json bytes.
func LoadConfigFromYamlBytes(content []byte, v interface{}) error { // Deprecated: use LoadFromJsonBytes instead.
func LoadConfigFromJsonBytes(content []byte, v interface{}) error {
return LoadFromJsonBytes(content, v)
}
// LoadFromTomlBytes loads config into v from content toml bytes.
func LoadFromTomlBytes(content []byte, v interface{}) error {
return mapping.UnmarshalTomlBytes(content, v)
}
// LoadFromYamlBytes loads config into v from content yaml bytes.
func LoadFromYamlBytes(content []byte, v interface{}) error {
return mapping.UnmarshalYamlBytes(content, v) return mapping.UnmarshalYamlBytes(content, v)
} }
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
// Deprecated: use LoadFromYamlBytes instead.
func LoadConfigFromYamlBytes(content []byte, v interface{}) error {
return LoadFromYamlBytes(content, v)
}
// MustLoad loads config into v from path, exits on error. // MustLoad loads config into v from path, exits on error.
func MustLoad(path string, v interface{}, opts ...Option) { func MustLoad(path string, v interface{}, opts ...Option) {
if err := LoadConfig(path, v, opts...); err != nil { if err := Load(path, v, opts...); err != nil {
log.Fatalf("error: config file %s, %s", path, err.Error()) log.Fatalf("error: config file %s, %s", path, err.Error())
} }
} }

View File

@@ -1,7 +1,6 @@
package conf package conf
import ( import (
"io/ioutil"
"os" "os"
"testing" "testing"
@@ -11,14 +10,14 @@ import (
) )
func TestLoadConfig_notExists(t *testing.T) { func TestLoadConfig_notExists(t *testing.T) {
assert.NotNil(t, LoadConfig("not_a_file", nil)) assert.NotNil(t, Load("not_a_file", nil))
} }
func TestLoadConfig_notRecogFile(t *testing.T) { func TestLoadConfig_notRecogFile(t *testing.T) {
filename, err := fs.TempFilenameWithText("hello") filename, err := fs.TempFilenameWithText("hello")
assert.Nil(t, err) assert.Nil(t, err)
defer os.Remove(filename) defer os.Remove(filename)
assert.NotNil(t, LoadConfig(filename, nil)) assert.NotNil(t, Load(filename, nil))
} }
func TestConfigJson(t *testing.T) { func TestConfigJson(t *testing.T) {
@@ -57,6 +56,58 @@ func TestConfigJson(t *testing.T) {
} }
} }
func TestConfigToml(t *testing.T) {
text := `a = "foo"
b = 1
c = "${FOO}"
d = "abcd!@#$112"
`
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
tmpfile, err := createTempFile(".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
B int `json:"b"`
C string `json:"c"`
D string `json:"d"`
}
MustLoad(tmpfile, &val)
assert.Equal(t, "foo", val.A)
assert.Equal(t, 1, val.B)
assert.Equal(t, "${FOO}", val.C)
assert.Equal(t, "abcd!@#$112", val.D)
}
func TestConfigTomlEnv(t *testing.T) {
text := `a = "foo"
b = 1
c = "${FOO}"
d = "abcd!@#112"
`
os.Setenv("FOO", "2")
defer os.Unsetenv("FOO")
tmpfile, err := createTempFile(".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)
var val struct {
A string `json:"a"`
B int `json:"b"`
C string `json:"c"`
D string `json:"d"`
}
MustLoad(tmpfile, &val, UseEnv())
assert.Equal(t, "foo", val.A)
assert.Equal(t, 1, val.B)
assert.Equal(t, "2", val.C)
assert.Equal(t, "abcd!@#112", val.D)
}
func TestConfigJsonEnv(t *testing.T) { func TestConfigJsonEnv(t *testing.T) {
tests := []string{ tests := []string{
".json", ".json",
@@ -94,12 +145,12 @@ func TestConfigJsonEnv(t *testing.T) {
} }
func createTempFile(ext, text string) (string, error) { func createTempFile(ext, text string) (string, error) {
tmpfile, err := ioutil.TempFile(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext) tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := ioutil.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil { if err := os.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil {
return "", err return "", err
} }

57
core/conf/readme.md Normal file
View File

@@ -0,0 +1,57 @@
## How to use
1. Define a config structure, like below:
```go
type RestfulConf struct {
Host string `json:",default=0.0.0.0"`
Port int
LogMode string `json:",options=[file,console]"`
Verbose bool `json:",optional"`
MaxConns int `json:",default=10000"`
MaxBytes int64 `json:",default=1048576"`
Timeout time.Duration `json:",default=3s"`
CpuThreshold int64 `json:",default=900,range=[0:1000]"`
}
```
2. Write the yaml, toml or json config file:
- yaml example
```yaml
# most fields are optional or have default values
Port: 8080
LogMode: console
# you can use env settings
MaxBytes: ${MAX_BYTES}
```
- toml example
```toml
# most fields are optional or have default values
Port = 8_080
LogMode = "console"
# you can use env settings
MaxBytes = "${MAX_BYTES}"
```
3. Load the config from a file:
```go
// exit on error
var config RestfulConf
conf.MustLoad(configFile, &config)
// or handle the error on your own
var config RestfulConf
if err := conf.Load(configFile, &config); err != nil {
log.Fatal(err)
}
// enable reading from environments
var config RestfulConf
conf.MustLoad(configFile, &config, conf.UseEnv())
```

View File

@@ -3,7 +3,7 @@ package internal
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"io/ioutil" "os"
"sync" "sync"
) )
@@ -37,7 +37,7 @@ func AddTLS(endpoints []string, certFile, certKeyFile, caFile string, insecureSk
return err return err
} }
caData, err := ioutil.ReadFile(caFile) caData, err := os.ReadFile(caFile)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -191,9 +191,11 @@ func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
}) })
} }
case clientv3.EventTypeDelete: case clientv3.EventTypeDelete:
c.lock.Lock()
if vals, ok := c.values[key]; ok { if vals, ok := c.values[key]; ok {
delete(vals, string(ev.Kv.Key)) delete(vals, string(ev.Kv.Key))
} }
c.lock.Unlock()
for _, l := range listeners { for _, l := range listeners {
l.OnDelete(KV{ l.OnDelete(KV{
Key: string(ev.Kv.Key), Key: string(ev.Kv.Key),
@@ -206,7 +208,7 @@ func (c *cluster) handleWatchEvents(key string, events []*clientv3.Event) {
} }
} }
func (c *cluster) load(cli EtcdClient, key string) { func (c *cluster) load(cli EtcdClient, key string) int64 {
var resp *clientv3.GetResponse var resp *clientv3.GetResponse
for { for {
var err error var err error
@@ -230,6 +232,8 @@ func (c *cluster) load(cli EtcdClient, key string) {
} }
c.handleChanges(key, kvs) c.handleChanges(key, kvs)
return resp.Header.Revision
} }
func (c *cluster) monitor(key string, l UpdateListener) error { func (c *cluster) monitor(key string, l UpdateListener) error {
@@ -242,9 +246,9 @@ func (c *cluster) monitor(key string, l UpdateListener) error {
return err return err
} }
c.load(cli, key) rev := c.load(cli, key)
c.watchGroup.Run(func() { c.watchGroup.Run(func() {
c.watch(cli, key) c.watch(cli, key, rev)
}) })
return nil return nil
@@ -276,22 +280,28 @@ func (c *cluster) reload(cli EtcdClient) {
for _, key := range keys { for _, key := range keys {
k := key k := key
c.watchGroup.Run(func() { c.watchGroup.Run(func() {
c.load(cli, k) rev := c.load(cli, k)
c.watch(cli, k) c.watch(cli, k, rev)
}) })
} }
} }
func (c *cluster) watch(cli EtcdClient, key string) { func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
for { for {
if c.watchStream(cli, key) { if c.watchStream(cli, key, rev) {
return return
} }
} }
} }
func (c *cluster) watchStream(cli EtcdClient, key string) bool { func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) bool {
rch := cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix()) 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())
}
for { for {
select { select {
case wresp, ok := <-rch: case wresp, ok := <-rch:

View File

@@ -2,6 +2,7 @@ package internal
import ( import (
"context" "context"
"go.etcd.io/etcd/api/v3/etcdserverpb"
"sync" "sync"
"testing" "testing"
@@ -112,6 +113,7 @@ func TestCluster_Load(t *testing.T) {
restore := setMockClient(cli) restore := setMockClient(cli)
defer restore() defer restore()
cli.EXPECT().Get(gomock.Any(), "any/", gomock.Any()).Return(&clientv3.GetResponse{ cli.EXPECT().Get(gomock.Any(), "any/", gomock.Any()).Return(&clientv3.GetResponse{
Header: &etcdserverpb.ResponseHeader{},
Kvs: []*mvccpb.KeyValue{ Kvs: []*mvccpb.KeyValue{
{ {
Key: []byte("hello"), Key: []byte("hello"),
@@ -168,7 +170,7 @@ func TestCluster_Watch(t *testing.T) {
listener.EXPECT().OnDelete(gomock.Any()).Do(func(_ interface{}) { listener.EXPECT().OnDelete(gomock.Any()).Do(func(_ interface{}) {
wg.Done() wg.Done()
}).MaxTimes(1) }).MaxTimes(1)
go c.watch(cli, "any") go c.watch(cli, "any", 0)
ch <- clientv3.WatchResponse{ ch <- clientv3.WatchResponse{
Events: []*clientv3.Event{ Events: []*clientv3.Event{
{ {
@@ -212,7 +214,7 @@ func TestClusterWatch_RespFailures(t *testing.T) {
ch <- resp ch <- resp
close(c.done) close(c.done)
}() }()
c.watch(cli, "any") c.watch(cli, "any", 0)
}) })
} }
} }
@@ -232,7 +234,7 @@ func TestClusterWatch_CloseChan(t *testing.T) {
close(ch) close(ch)
close(c.done) close(c.done)
}() }()
c.watch(cli, "any") c.watch(cli, "any", 0)
} }
func TestValueOnlyContext(t *testing.T) { func TestValueOnlyContext(t *testing.T) {

21
core/errorx/wrap.go Normal file
View File

@@ -0,0 +1,21 @@
package errorx
import "fmt"
// Wrap returns an error that wraps err with given message.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", message, err)
}
// Wrapf returns an error that wraps err with given format and args.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

24
core/errorx/wrap_test.go Normal file
View File

@@ -0,0 +1,24 @@
package errorx
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWrap(t *testing.T) {
assert.Nil(t, Wrap(nil, "test"))
assert.Equal(t, "foo: bar", Wrap(errors.New("bar"), "foo").Error())
err := errors.New("foo")
assert.True(t, errors.Is(Wrap(err, "bar"), err))
}
func TestWrapf(t *testing.T) {
assert.Nil(t, Wrapf(nil, "%s", "test"))
assert.Equal(t, "foo bar: quz", Wrapf(errors.New("quz"), "foo %s", "bar").Error())
err := errors.New("foo")
assert.True(t, errors.Is(Wrapf(err, "foo %s", "bar"), err))
}

View File

@@ -1,7 +1,6 @@
package fs package fs
import ( import (
"io/ioutil"
"os" "os"
"github.com/zeromicro/go-zero/core/hash" "github.com/zeromicro/go-zero/core/hash"
@@ -12,12 +11,12 @@ import (
// The file is kept as open, the caller should close the file handle, // The file is kept as open, the caller should close the file handle,
// and remove the file by name. // and remove the file by name.
func TempFileWithText(text string) (*os.File, error) { func TempFileWithText(text string) (*os.File, error) {
tmpfile, err := ioutil.TempFile(os.TempDir(), hash.Md5Hex([]byte(text))) tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := ioutil.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil { if err := os.WriteFile(tmpfile.Name(), []byte(text), os.ModeTemporary); err != nil {
return nil, err return nil, err
} }

49
core/fs/temps_test.go Normal file
View File

@@ -0,0 +1,49 @@
package fs
import (
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTempFileWithText(t *testing.T) {
f, err := TempFileWithText("test")
if err != nil {
t.Error(err)
}
if f == nil {
t.Error("TempFileWithText returned nil")
}
if f.Name() == "" {
t.Error("TempFileWithText returned empty file name")
}
defer os.Remove(f.Name())
bs, err := io.ReadAll(f)
assert.Nil(t, err)
if len(bs) != 4 {
t.Error("TempFileWithText returned wrong file size")
}
if f.Close() != nil {
t.Error("TempFileWithText returned error on close")
}
}
func TestTempFilenameWithText(t *testing.T) {
f, err := TempFilenameWithText("test")
if err != nil {
t.Error(err)
}
if f == "" {
t.Error("TempFilenameWithText returned empty file name")
}
defer os.Remove(f)
bs, err := os.ReadFile(f)
assert.Nil(t, err)
if len(bs) != 4 {
t.Error("TempFilenameWithText returned wrong file size")
}
}

View File

@@ -1,7 +1,7 @@
package fx package fx
import ( import (
"io/ioutil" "io"
"log" "log"
"math/rand" "math/rand"
"reflect" "reflect"
@@ -238,7 +238,7 @@ func TestLast(t *testing.T) {
func TestMap(t *testing.T) { func TestMap(t *testing.T) {
runCheckedTest(t, func(t *testing.T) { runCheckedTest(t, func(t *testing.T) {
log.SetOutput(ioutil.Discard) log.SetOutput(io.Discard)
tests := []struct { tests := []struct {
mapper MapFunc mapper MapFunc

View File

@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"io" "io"
"io/ioutil"
"os" "os"
"strings" "strings"
) )
@@ -26,7 +25,7 @@ type (
func DupReadCloser(reader io.ReadCloser) (io.ReadCloser, io.ReadCloser) { func DupReadCloser(reader io.ReadCloser) (io.ReadCloser, io.ReadCloser) {
var buf bytes.Buffer var buf bytes.Buffer
tee := io.TeeReader(reader, &buf) tee := io.TeeReader(reader, &buf)
return ioutil.NopCloser(tee), ioutil.NopCloser(&buf) return io.NopCloser(tee), io.NopCloser(&buf)
} }
// KeepSpace customizes the reading functions to keep leading and tailing spaces. // KeepSpace customizes the reading functions to keep leading and tailing spaces.
@@ -54,7 +53,7 @@ func ReadBytes(reader io.Reader, buf []byte) error {
// ReadText reads content from the given file with leading and tailing spaces trimmed. // ReadText reads content from the given file with leading and tailing spaces trimmed.
func ReadText(filename string) (string, error) { func ReadText(filename string) (string, error) {
content, err := ioutil.ReadFile(filename) content, err := os.ReadFile(filename)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -3,7 +3,6 @@ package iox
import ( import (
"bytes" "bytes"
"io" "io"
"io/ioutil"
"os" "os"
"testing" "testing"
"time" "time"
@@ -97,10 +96,10 @@ func TestReadTextLines(t *testing.T) {
func TestDupReadCloser(t *testing.T) { func TestDupReadCloser(t *testing.T) {
input := "hello" input := "hello"
reader := ioutil.NopCloser(bytes.NewBufferString(input)) reader := io.NopCloser(bytes.NewBufferString(input))
r1, r2 := DupReadCloser(reader) r1, r2 := DupReadCloser(reader)
verify := func(r io.Reader) { verify := func(r io.Reader) {
output, err := ioutil.ReadAll(r) output, err := io.ReadAll(r)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, input, string(output)) assert.Equal(t, input, string(output))
} }
@@ -110,7 +109,7 @@ func TestDupReadCloser(t *testing.T) {
} }
func TestReadBytes(t *testing.T) { func TestReadBytes(t *testing.T) {
reader := ioutil.NopCloser(bytes.NewBufferString("helloworld")) reader := io.NopCloser(bytes.NewBufferString("helloworld"))
buf := make([]byte, 5) buf := make([]byte, 5)
err := ReadBytes(reader, buf) err := ReadBytes(reader, buf)
assert.Nil(t, err) assert.Nil(t, err)
@@ -118,7 +117,7 @@ func TestReadBytes(t *testing.T) {
} }
func TestReadBytesNotEnough(t *testing.T) { func TestReadBytesNotEnough(t *testing.T) {
reader := ioutil.NopCloser(bytes.NewBufferString("hell")) reader := io.NopCloser(bytes.NewBufferString("hell"))
buf := make([]byte, 5) buf := make([]byte, 5)
err := ReadBytes(reader, buf) err := ReadBytes(reader, buf)
assert.Equal(t, io.EOF, err) assert.Equal(t, io.EOF, err)

View File

@@ -1,7 +1,6 @@
package iox package iox
import ( import (
"io/ioutil"
"os" "os"
"testing" "testing"
@@ -13,7 +12,7 @@ func TestCountLines(t *testing.T) {
2 2
3 3
4` 4`
file, err := ioutil.TempFile(os.TempDir(), "test-") file, err := os.CreateTemp(os.TempDir(), "test-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -13,6 +13,16 @@ func Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v) return json.Marshal(v)
} }
// MarshalToString marshals v into a string.
func MarshalToString(v interface{}) (string, error) {
data, err := Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}
// Unmarshal unmarshals data bytes into v. // Unmarshal unmarshals data bytes into v.
func Unmarshal(data []byte, v interface{}) error { func Unmarshal(data []byte, v interface{}) error {
decoder := json.NewDecoder(bytes.NewReader(data)) decoder := json.NewDecoder(bytes.NewReader(data))

103
core/jsonx/json_test.go Normal file
View File

@@ -0,0 +1,103 @@
package jsonx
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMarshal(t *testing.T) {
var v = struct {
Name string `json:"name"`
Age int `json:"age"`
}{
Name: "John",
Age: 30,
}
bs, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, `{"name":"John","age":30}`, string(bs))
}
func TestMarshalToString(t *testing.T) {
var v = struct {
Name string `json:"name"`
Age int `json:"age"`
}{
Name: "John",
Age: 30,
}
toString, err := MarshalToString(v)
assert.Nil(t, err)
assert.Equal(t, `{"name":"John","age":30}`, toString)
_, err = MarshalToString(make(chan int))
assert.NotNil(t, err)
}
func TestUnmarshal(t *testing.T) {
const s = `{"name":"John","age":30}`
var v struct {
Name string `json:"name"`
Age int `json:"age"`
}
err := Unmarshal([]byte(s), &v)
assert.Nil(t, err)
assert.Equal(t, "John", v.Name)
assert.Equal(t, 30, v.Age)
}
func TestUnmarshalError(t *testing.T) {
const s = `{"name":"John","age":30`
var v struct {
Name string `json:"name"`
Age int `json:"age"`
}
err := Unmarshal([]byte(s), &v)
assert.NotNil(t, err)
}
func TestUnmarshalFromString(t *testing.T) {
const s = `{"name":"John","age":30}`
var v struct {
Name string `json:"name"`
Age int `json:"age"`
}
err := UnmarshalFromString(s, &v)
assert.Nil(t, err)
assert.Equal(t, "John", v.Name)
assert.Equal(t, 30, v.Age)
}
func TestUnmarshalFromStringError(t *testing.T) {
const s = `{"name":"John","age":30`
var v struct {
Name string `json:"name"`
Age int `json:"age"`
}
err := UnmarshalFromString(s, &v)
assert.NotNil(t, err)
}
func TestUnmarshalFromRead(t *testing.T) {
const s = `{"name":"John","age":30}`
var v struct {
Name string `json:"name"`
Age int `json:"age"`
}
err := UnmarshalFromReader(strings.NewReader(s), &v)
assert.Nil(t, err)
assert.Equal(t, "John", v.Name)
assert.Equal(t, 30, v.Age)
}
func TestUnmarshalFromReaderError(t *testing.T) {
const s = `{"name":"John","age":30`
var v struct {
Name string `json:"name"`
Age int `json:"age"`
}
err := UnmarshalFromReader(strings.NewReader(s), &v)
assert.NotNil(t, err)
}

View File

@@ -1,6 +1,7 @@
package limit package limit
import ( import (
"context"
"errors" "errors"
"strconv" "strconv"
"time" "time"
@@ -74,7 +75,12 @@ func NewPeriodLimit(period, quota int, limitStore *redis.Redis, keyPrefix string
// Take requests a permit, it returns the permit state. // Take requests a permit, it returns the permit state.
func (h *PeriodLimit) Take(key string) (int, error) { func (h *PeriodLimit) Take(key string) (int, error) {
resp, err := h.limitStore.Eval(periodScript, []string{h.keyPrefix + key}, []string{ return h.TakeCtx(context.Background(), key)
}
// TakeCtx requests a permit with context, it returns the permit state.
func (h *PeriodLimit) TakeCtx(ctx context.Context, key string) (int, error) {
resp, err := h.limitStore.EvalCtx(ctx, periodScript, []string{h.keyPrefix + key}, []string{
strconv.Itoa(h.quota), strconv.Itoa(h.quota),
strconv.Itoa(h.calcExpireSeconds()), strconv.Itoa(h.calcExpireSeconds()),
}) })

26
core/logx/color.go Normal file
View File

@@ -0,0 +1,26 @@
package logx
import (
"sync/atomic"
"github.com/zeromicro/go-zero/core/color"
)
// WithColor is a helper function to add color to a string, only in plain encoding.
func WithColor(text string, colour color.Color) string {
if atomic.LoadUint32(&encoding) == plainEncodingType {
return color.WithColor(text, colour)
}
return text
}
// WithColorPadding is a helper function to add color to a string with leading and trailing spaces,
// only in plain encoding.
func WithColorPadding(text string, colour color.Color) string {
if atomic.LoadUint32(&encoding) == plainEncodingType {
return color.WithColorPadding(text, colour)
}
return text
}

33
core/logx/color_test.go Normal file
View File

@@ -0,0 +1,33 @@
package logx
import (
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/color"
)
func TestWithColor(t *testing.T) {
old := atomic.SwapUint32(&encoding, plainEncodingType)
defer atomic.StoreUint32(&encoding, old)
output := WithColor("hello", color.BgBlue)
assert.Equal(t, "hello", output)
atomic.StoreUint32(&encoding, jsonEncodingType)
output = WithColor("hello", color.BgBlue)
assert.Equal(t, "hello", output)
}
func TestWithColorPadding(t *testing.T) {
old := atomic.SwapUint32(&encoding, plainEncodingType)
defer atomic.StoreUint32(&encoding, old)
output := WithColorPadding("hello", color.BgBlue)
assert.Equal(t, " hello ", output)
atomic.StoreUint32(&encoding, jsonEncodingType)
output = WithColorPadding("hello", color.BgBlue)
assert.Equal(t, "hello", output)
}

View File

@@ -11,4 +11,16 @@ type LogConf struct {
Compress bool `json:",optional"` Compress bool `json:",optional"`
KeepDays int `json:",optional"` KeepDays int `json:",optional"`
StackCooldownMillis int `json:",default=100"` StackCooldownMillis int `json:",default=100"`
// MaxBackups represents how many backup log files will be kept. 0 means all files will be kept forever.
// Only take effect when RotationRuleType is `size`.
// Even thougth `MaxBackups` sets 0, log files will still be removed
// if the `KeepDays` limitation is reached.
MaxBackups int `json:",default=0"`
// MaxSize represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`.
// Only take effect when RotationRuleType is `size`
MaxSize int `json:",default=0"`
// RotationRuleType represents the type of log rotation rule. Default is `daily`.
// daily: daily rotation.
// size: size limited rotation.
Rotation string `json:",default=daily,options=[daily,size]"`
} }

145
core/logx/contextlogger.go Normal file
View File

@@ -0,0 +1,145 @@
package logx
import (
"context"
"fmt"
"time"
"github.com/zeromicro/go-zero/core/timex"
"go.opentelemetry.io/otel/trace"
)
// WithContext sets ctx to log, for keeping tracing information.
func WithContext(ctx context.Context) Logger {
return &contextLogger{
ctx: ctx,
}
}
type contextLogger struct {
logEntry
ctx context.Context
}
func (l *contextLogger) Error(v ...interface{}) {
l.err(fmt.Sprint(v...))
}
func (l *contextLogger) Errorf(format string, v ...interface{}) {
l.err(fmt.Sprintf(format, v...))
}
func (l *contextLogger) Errorv(v interface{}) {
l.err(fmt.Sprint(v))
}
func (l *contextLogger) Errorw(msg string, fields ...LogField) {
l.err(msg, fields...)
}
func (l *contextLogger) Info(v ...interface{}) {
l.info(fmt.Sprint(v...))
}
func (l *contextLogger) Infof(format string, v ...interface{}) {
l.info(fmt.Sprintf(format, v...))
}
func (l *contextLogger) Infov(v interface{}) {
l.info(v)
}
func (l *contextLogger) Infow(msg string, fields ...LogField) {
l.info(msg, fields...)
}
func (l *contextLogger) Slow(v ...interface{}) {
l.slow(fmt.Sprint(v...))
}
func (l *contextLogger) Slowf(format string, v ...interface{}) {
l.slow(fmt.Sprintf(format, v...))
}
func (l *contextLogger) Slowv(v interface{}) {
l.slow(v)
}
func (l *contextLogger) Sloww(msg string, fields ...LogField) {
l.slow(msg, fields...)
}
func (l *contextLogger) WithContext(ctx context.Context) Logger {
if ctx == nil {
return l
}
l.ctx = ctx
return l
}
func (l *contextLogger) WithDuration(duration time.Duration) Logger {
l.Duration = timex.ReprOfDuration(duration)
return l
}
func (l *contextLogger) buildFields(fields ...LogField) []LogField {
if len(l.Duration) > 0 {
fields = append(fields, Field(durationKey, l.Duration))
}
traceID := traceIdFromContext(l.ctx)
if len(traceID) > 0 {
fields = append(fields, Field(traceKey, traceID))
}
spanID := spanIdFromContext(l.ctx)
if len(spanID) > 0 {
fields = append(fields, Field(spanKey, spanID))
}
val := l.ctx.Value(fieldsContextKey)
if val != nil {
if arr, ok := val.([]LogField); ok {
fields = append(fields, arr...)
}
}
return fields
}
func (l *contextLogger) err(v interface{}, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Error(v, l.buildFields(fields...)...)
}
}
func (l *contextLogger) info(v interface{}, fields ...LogField) {
if shallLog(InfoLevel) {
getWriter().Info(v, l.buildFields(fields...)...)
}
}
func (l *contextLogger) slow(v interface{}, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Slow(v, l.buildFields(fields...)...)
}
}
func spanIdFromContext(ctx context.Context) string {
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.HasSpanID() {
return spanCtx.SpanID().String()
}
return ""
}
func traceIdFromContext(ctx context.Context) string {
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.HasTraceID() {
return spanCtx.TraceID().String()
}
return ""
}

View File

@@ -0,0 +1,240 @@
package logx
import (
"context"
"encoding/json"
"io"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func TestTraceLog(t *testing.T) {
SetLevel(InfoLevel)
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id")
defer span.End()
WithContext(ctx).Info(testlog)
validate(t, w.String(), true, true)
}
func TestTraceError(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id")
defer span.End()
var nilCtx context.Context
l := WithContext(context.Background())
l = l.WithContext(nilCtx)
l = l.WithContext(ctx)
SetLevel(ErrorLevel)
l.WithDuration(time.Second).Error(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Errorf(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Errorv(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Errorw(testlog, Field("basket", "ball"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "basket"), w.String())
assert.True(t, strings.Contains(w.String(), "ball"), w.String())
}
func TestTraceInfo(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id")
defer span.End()
SetLevel(InfoLevel)
l := WithContext(ctx)
l.WithDuration(time.Second).Info(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infof(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infov(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infow(testlog, Field("basket", "ball"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "basket"), w.String())
assert.True(t, strings.Contains(w.String(), "ball"), w.String())
}
func TestTraceInfoConsole(t *testing.T) {
old := atomic.SwapUint32(&encoding, jsonEncodingType)
defer atomic.StoreUint32(&encoding, old)
w := new(mockWriter)
o := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(o)
}()
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id")
defer span.End()
l := WithContext(ctx)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infof(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Infov(testlog)
validate(t, w.String(), true, true)
}
func TestTraceSlow(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, span := tp.Tracer("trace-id").Start(context.Background(), "span-id")
defer span.End()
l := WithContext(ctx)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Slow(testlog)
assert.True(t, strings.Contains(w.String(), traceKey))
assert.True(t, strings.Contains(w.String(), spanKey))
w.Reset()
l.WithDuration(time.Second).Slowf(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Slowv(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Sloww(testlog, Field("basket", "ball"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "basket"), w.String())
assert.True(t, strings.Contains(w.String(), "ball"), w.String())
}
func TestTraceWithoutContext(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
l := WithContext(context.Background())
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
validate(t, w.String(), false, false)
w.Reset()
l.WithDuration(time.Second).Infof(testlog)
validate(t, w.String(), false, false)
}
func TestLogWithFields(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
ctx := WithFields(context.Background(), Field("foo", "bar"))
l := WithContext(ctx)
SetLevel(InfoLevel)
l.Info(testlog)
var val mockValue
assert.Nil(t, json.Unmarshal([]byte(w.String()), &val))
assert.Equal(t, "bar", val.Foo)
}
func validate(t *testing.T, body string, expectedTrace, expectedSpan bool) {
var val mockValue
dec := json.NewDecoder(strings.NewReader(body))
for {
var doc mockValue
err := dec.Decode(&doc)
if err == io.EOF {
// all done
break
}
if err != nil {
continue
}
val = doc
}
assert.Equal(t, expectedTrace, len(val.Trace) > 0, body)
assert.Equal(t, expectedSpan, len(val.Span) > 0, body)
}
type mockValue struct {
Trace string `json:"trace"`
Span string `json:"span"`
Foo string `json:"foo"`
}

View File

@@ -1,18 +1,13 @@
package logx package logx
import ( import (
"context"
"fmt" "fmt"
"io"
"sync/atomic"
"time" "time"
"github.com/zeromicro/go-zero/core/timex" "github.com/zeromicro/go-zero/core/timex"
) )
const durationCallerDepth = 3
type durationLogger logEntry
// WithDuration returns a Logger which logs the given duration. // WithDuration returns a Logger which logs the given duration.
func WithDuration(d time.Duration) Logger { func WithDuration(d time.Duration) Logger {
return &durationLogger{ return &durationLogger{
@@ -20,57 +15,62 @@ func WithDuration(d time.Duration) Logger {
} }
} }
type durationLogger logEntry
func (l *durationLogger) Error(v ...interface{}) { func (l *durationLogger) Error(v ...interface{}) {
if shallLog(ErrorLevel) { l.err(fmt.Sprint(v...))
l.write(errorLog, levelError, formatWithCaller(fmt.Sprint(v...), durationCallerDepth))
}
} }
func (l *durationLogger) Errorf(format string, v ...interface{}) { func (l *durationLogger) Errorf(format string, v ...interface{}) {
if shallLog(ErrorLevel) { l.err(fmt.Sprintf(format, v...))
l.write(errorLog, levelError, formatWithCaller(fmt.Sprintf(format, v...), durationCallerDepth))
}
} }
func (l *durationLogger) Errorv(v interface{}) { func (l *durationLogger) Errorv(v interface{}) {
if shallLog(ErrorLevel) { l.err(v)
l.write(errorLog, levelError, v) }
}
func (l *durationLogger) Errorw(msg string, fields ...LogField) {
l.err(msg, fields...)
} }
func (l *durationLogger) Info(v ...interface{}) { func (l *durationLogger) Info(v ...interface{}) {
if shallLog(InfoLevel) { l.info(fmt.Sprint(v...))
l.write(infoLog, levelInfo, fmt.Sprint(v...))
}
} }
func (l *durationLogger) Infof(format string, v ...interface{}) { func (l *durationLogger) Infof(format string, v ...interface{}) {
if shallLog(InfoLevel) { l.info(fmt.Sprintf(format, v...))
l.write(infoLog, levelInfo, fmt.Sprintf(format, v...))
}
} }
func (l *durationLogger) Infov(v interface{}) { func (l *durationLogger) Infov(v interface{}) {
if shallLog(InfoLevel) { l.info(v)
l.write(infoLog, levelInfo, v) }
}
func (l *durationLogger) Infow(msg string, fields ...LogField) {
l.info(msg, fields...)
} }
func (l *durationLogger) Slow(v ...interface{}) { func (l *durationLogger) Slow(v ...interface{}) {
if shallLog(ErrorLevel) { l.slow(fmt.Sprint(v...))
l.write(slowLog, levelSlow, fmt.Sprint(v...))
}
} }
func (l *durationLogger) Slowf(format string, v ...interface{}) { func (l *durationLogger) Slowf(format string, v ...interface{}) {
if shallLog(ErrorLevel) { l.slow(fmt.Sprintf(format, v...))
l.write(slowLog, levelSlow, fmt.Sprintf(format, v...))
}
} }
func (l *durationLogger) Slowv(v interface{}) { func (l *durationLogger) Slowv(v interface{}) {
if shallLog(ErrorLevel) { l.slow(v)
l.write(slowLog, levelSlow, v) }
func (l *durationLogger) Sloww(msg string, fields ...LogField) {
l.slow(msg, fields...)
}
func (l *durationLogger) WithContext(ctx context.Context) Logger {
return &contextLogger{
ctx: ctx,
logEntry: logEntry{
Duration: l.Duration,
},
} }
} }
@@ -79,16 +79,23 @@ func (l *durationLogger) WithDuration(duration time.Duration) Logger {
return l return l
} }
func (l *durationLogger) write(writer io.Writer, level string, val interface{}) { func (l *durationLogger) err(v interface{}, fields ...LogField) {
switch atomic.LoadUint32(&encoding) { if shallLog(ErrorLevel) {
case plainEncodingType: fields = append(fields, Field(durationKey, l.Duration))
writePlainAny(writer, level, val, l.Duration) getWriter().Error(v, fields...)
default: }
outputJson(writer, &durationLogger{ }
Timestamp: getTimestamp(),
Level: level, func (l *durationLogger) info(v interface{}, fields ...LogField) {
Content: val, if shallLog(InfoLevel) {
Duration: l.Duration, fields = append(fields, Field(durationKey, l.Duration))
}) getWriter().Info(v, fields...)
}
}
func (l *durationLogger) slow(v interface{}, fields ...LogField) {
if shallLog(ErrorLevel) {
fields = append(fields, Field(durationKey, l.Duration))
getWriter().Slow(v, fields...)
} }
} }

View File

@@ -1,41 +1,62 @@
package logx package logx
import ( import (
"log" "context"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
) )
func TestWithDurationError(t *testing.T) { func TestWithDurationError(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Error("foo") WithDuration(time.Second).Error("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
} }
func TestWithDurationErrorf(t *testing.T) { func TestWithDurationErrorf(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Errorf("foo") WithDuration(time.Second).Errorf("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
} }
func TestWithDurationErrorv(t *testing.T) { func TestWithDurationErrorv(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Errorv("foo") WithDuration(time.Second).Errorv("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
}
func TestWithDurationErrorw(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Errorw("foo", Field("foo", "bar"))
assert.True(t, strings.Contains(w.String(), "duration"), w.String())
assert.True(t, strings.Contains(w.String(), "foo"), w.String())
assert.True(t, strings.Contains(w.String(), "bar"), w.String())
} }
func TestWithDurationInfo(t *testing.T) { func TestWithDurationInfo(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Info("foo") WithDuration(time.Second).Info("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
} }
func TestWithDurationInfoConsole(t *testing.T) { func TestWithDurationInfoConsole(t *testing.T) {
@@ -45,43 +66,96 @@ func TestWithDurationInfoConsole(t *testing.T) {
atomic.StoreUint32(&encoding, old) atomic.StoreUint32(&encoding, old)
}() }()
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) o := writer.Swap(w)
defer writer.Store(o)
WithDuration(time.Second).Info("foo") WithDuration(time.Second).Info("foo")
assert.True(t, strings.Contains(builder.String(), "ms"), builder.String()) assert.True(t, strings.Contains(w.String(), "ms"), w.String())
} }
func TestWithDurationInfof(t *testing.T) { func TestWithDurationInfof(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Infof("foo") WithDuration(time.Second).Infof("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
} }
func TestWithDurationInfov(t *testing.T) { func TestWithDurationInfov(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Infov("foo") WithDuration(time.Second).Infov("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
}
func TestWithDurationInfow(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Infow("foo", Field("foo", "bar"))
assert.True(t, strings.Contains(w.String(), "duration"), w.String())
assert.True(t, strings.Contains(w.String(), "foo"), w.String())
assert.True(t, strings.Contains(w.String(), "bar"), w.String())
}
func TestWithDurationWithContextInfow(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
WithDuration(time.Second).WithContext(ctx).Infow("foo", Field("foo", "bar"))
assert.True(t, strings.Contains(w.String(), "duration"), w.String())
assert.True(t, strings.Contains(w.String(), "foo"), w.String())
assert.True(t, strings.Contains(w.String(), "bar"), w.String())
assert.True(t, strings.Contains(w.String(), "trace"), w.String())
assert.True(t, strings.Contains(w.String(), "span"), w.String())
} }
func TestWithDurationSlow(t *testing.T) { func TestWithDurationSlow(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Slow("foo") WithDuration(time.Second).Slow("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
} }
func TestWithDurationSlowf(t *testing.T) { func TestWithDurationSlowf(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).WithDuration(time.Hour).Slowf("foo") WithDuration(time.Second).WithDuration(time.Hour).Slowf("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
} }
func TestWithDurationSlowv(t *testing.T) { func TestWithDurationSlowv(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).WithDuration(time.Hour).Slowv("foo") WithDuration(time.Second).WithDuration(time.Hour).Slowv("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String()) assert.True(t, strings.Contains(w.String(), "duration"), w.String())
}
func TestWithDurationSloww(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).WithDuration(time.Hour).Sloww("foo", Field("foo", "bar"))
assert.True(t, strings.Contains(w.String(), "duration"), w.String())
assert.True(t, strings.Contains(w.String(), "foo"), w.String())
assert.True(t, strings.Contains(w.String(), "bar"), w.String())
} }

18
core/logx/fields.go Normal file
View File

@@ -0,0 +1,18 @@
package logx
import "context"
var fieldsContextKey contextKey
type contextKey struct{}
// WithFields returns a new context with the given fields.
func WithFields(ctx context.Context, fields ...LogField) context.Context {
if val := ctx.Value(fieldsContextKey); val != nil {
if arr, ok := val.([]LogField); ok {
return context.WithValue(ctx, fieldsContextKey, append(arr, fields...))
}
}
return context.WithValue(ctx, fieldsContextKey, fields)
}

35
core/logx/fields_test.go Normal file
View File

@@ -0,0 +1,35 @@
package logx
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithFields(t *testing.T) {
ctx := WithFields(context.Background(), Field("a", 1), Field("b", 2))
vals := ctx.Value(fieldsContextKey)
assert.NotNil(t, vals)
fields, ok := vals.([]LogField)
assert.True(t, ok)
assert.EqualValues(t, []LogField{Field("a", 1), Field("b", 2)}, fields)
}
func TestWithFieldsAppend(t *testing.T) {
var dummyKey struct{}
ctx := context.WithValue(context.Background(), dummyKey, "dummy")
ctx = WithFields(ctx, Field("a", 1), Field("b", 2))
ctx = WithFields(ctx, Field("c", 3), Field("d", 4))
vals := ctx.Value(fieldsContextKey)
assert.NotNil(t, vals)
fields, ok := vals.([]LogField)
assert.True(t, ok)
assert.Equal(t, "dummy", ctx.Value(dummyKey))
assert.EqualValues(t, []LogField{
Field("a", 1),
Field("b", 2),
Field("c", 3),
Field("d", 4),
}, fields)
}

View File

@@ -1,7 +1,6 @@
package logx package logx
import ( import (
"log"
"strings" "strings"
"testing" "testing"
@@ -9,23 +8,27 @@ import (
) )
func TestLessLogger_Error(t *testing.T) { func TestLessLogger_Error(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
l := NewLessLogger(500) l := NewLessLogger(500)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
l.Error("hello") l.Error("hello")
} }
assert.Equal(t, 1, strings.Count(builder.String(), "\n")) assert.Equal(t, 1, strings.Count(w.String(), "\n"))
} }
func TestLessLogger_Errorf(t *testing.T) { func TestLessLogger_Errorf(t *testing.T) {
var builder strings.Builder w := new(mockWriter)
log.SetOutput(&builder) old := writer.Swap(w)
defer writer.Store(old)
l := NewLessLogger(500) l := NewLessLogger(500)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
l.Errorf("hello") l.Errorf("hello")
} }
assert.Equal(t, 1, strings.Count(builder.String(), "\n")) assert.Equal(t, 1, strings.Count(w.String(), "\n"))
} }

38
core/logx/logger.go Normal file
View File

@@ -0,0 +1,38 @@
package logx
import (
"context"
"time"
)
// A Logger represents a logger.
type Logger interface {
// Error logs a message at error level.
Error(...interface{})
// Errorf logs a message at error level.
Errorf(string, ...interface{})
// Errorv logs a message at error level.
Errorv(interface{})
// Errorw logs a message at error level.
Errorw(string, ...LogField)
// Info logs a message at info level.
Info(...interface{})
// Infof logs a message at info level.
Infof(string, ...interface{})
// Infov logs a message at info level.
Infov(interface{})
// Infow logs a message at info level.
Infow(string, ...LogField)
// Slow logs a message at slow level.
Slow(...interface{})
// Slowf logs a message at slow level.
Slowf(string, ...interface{})
// Slowv logs a message at slow level.
Slowv(interface{})
// Sloww logs a message at slow level.
Sloww(string, ...LogField)
// WithContext returns a new logger with the given context.
WithContext(context.Context) Logger
// WithDuration returns a new logger with the given duration.
WithDuration(time.Duration) Logger
}

View File

@@ -1,93 +1,31 @@
package logx package logx
import ( import (
"bytes"
"encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"path" "path"
"runtime"
"runtime/debug" "runtime/debug"
"strconv"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/zeromicro/go-zero/core/iox"
"github.com/zeromicro/go-zero/core/sysx" "github.com/zeromicro/go-zero/core/sysx"
"github.com/zeromicro/go-zero/core/timex"
) )
const ( const callerDepth = 5
// InfoLevel logs everything
InfoLevel = iota
// ErrorLevel includes errors, slows, stacks
ErrorLevel
// SevereLevel only log severe messages
SevereLevel
)
const (
jsonEncodingType = iota
plainEncodingType
jsonEncoding = "json"
plainEncoding = "plain"
plainEncodingSep = '\t'
)
const (
accessFilename = "access.log"
errorFilename = "error.log"
severeFilename = "severe.log"
slowFilename = "slow.log"
statFilename = "stat.log"
consoleMode = "console"
volumeMode = "volume"
levelAlert = "alert"
levelInfo = "info"
levelError = "error"
levelSevere = "severe"
levelFatal = "fatal"
levelSlow = "slow"
levelStat = "stat"
backupFileDelimiter = "-"
callerInnerDepth = 5
flags = 0x0
)
var ( var (
// ErrLogPathNotSet is an error that indicates the log path is not set. timeFormat = "2006-01-02T15:04:05.000Z07:00"
ErrLogPathNotSet = errors.New("log path must be set") logLevel uint32
// ErrLogNotInitialized is an error that log is not initialized. encoding uint32 = jsonEncodingType
ErrLogNotInitialized = errors.New("log not initialized")
// ErrLogServiceNameNotSet is an error that indicates that the service name is not set.
ErrLogServiceNameNotSet = errors.New("log service name must be set")
timeFormat = "2006-01-02T15:04:05.000Z07:00"
writeConsole bool
logLevel uint32
encoding uint32 = jsonEncodingType
// use uint32 for atomic operations // use uint32 for atomic operations
disableLog uint32
disableStat uint32 disableStat uint32
infoLog io.WriteCloser
errorLog io.WriteCloser
severeLog io.WriteCloser
slowLog io.WriteCloser
statLog io.WriteCloser
stackLog io.Writer
once sync.Once
initialized uint32
options logOptions options logOptions
writer = new(atomicWriter)
setupOnce sync.Once
) )
type ( type (
@@ -95,109 +33,40 @@ type (
Timestamp string `json:"@timestamp"` Timestamp string `json:"@timestamp"`
Level string `json:"level"` Level string `json:"level"`
Duration string `json:"duration,omitempty"` Duration string `json:"duration,omitempty"`
Caller string `json:"caller,omitempty"`
Content interface{} `json:"content"` Content interface{} `json:"content"`
} }
logEntryWithFields map[string]interface{}
logOptions struct { logOptions struct {
gzipEnabled bool gzipEnabled bool
logStackCooldownMills int logStackCooldownMills int
keepDays int keepDays int
maxBackups int
maxSize int
rotationRule string
}
// LogField is a key-value pair that will be added to the log entry.
LogField struct {
Key string
Value interface{}
} }
// LogOption defines the method to customize the logging. // LogOption defines the method to customize the logging.
LogOption func(options *logOptions) LogOption func(options *logOptions)
// A Logger represents a logger.
Logger interface {
Error(...interface{})
Errorf(string, ...interface{})
Errorv(interface{})
Info(...interface{})
Infof(string, ...interface{})
Infov(interface{})
Slow(...interface{})
Slowf(string, ...interface{})
Slowv(interface{})
WithDuration(time.Duration) Logger
}
) )
// MustSetup sets up logging with given config c. It exits on error.
func MustSetup(c LogConf) {
Must(SetUp(c))
}
// SetUp sets up the logx. If already set up, just return nil.
// we allow SetUp to be called multiple times, because for example
// we need to allow different service frameworks to initialize logx respectively.
// the same logic for SetUp
func SetUp(c LogConf) error {
if len(c.TimeFormat) > 0 {
timeFormat = c.TimeFormat
}
switch c.Encoding {
case plainEncoding:
atomic.StoreUint32(&encoding, plainEncodingType)
default:
atomic.StoreUint32(&encoding, jsonEncodingType)
}
switch c.Mode {
case consoleMode:
setupWithConsole(c)
return nil
case volumeMode:
return setupWithVolume(c)
default:
return setupWithFiles(c)
}
}
// Alert alerts v in alert level, and the message is written to error log. // Alert alerts v in alert level, and the message is written to error log.
func Alert(v string) { func Alert(v string) {
outputText(errorLog, levelAlert, v) getWriter().Alert(v)
} }
// Close closes the logging. // Close closes the logging.
func Close() error { func Close() error {
if writeConsole { if w := writer.Swap(nil); w != nil {
return nil return w.(io.Closer).Close()
}
if atomic.LoadUint32(&initialized) == 0 {
return ErrLogNotInitialized
}
atomic.StoreUint32(&initialized, 0)
if infoLog != nil {
if err := infoLog.Close(); err != nil {
return err
}
}
if errorLog != nil {
if err := errorLog.Close(); err != nil {
return err
}
}
if severeLog != nil {
if err := severeLog.Close(); err != nil {
return err
}
}
if slowLog != nil {
if err := slowLog.Close(); err != nil {
return err
}
}
if statLog != nil {
if err := statLog.Close(); err != nil {
return err
}
} }
return nil return nil
@@ -205,16 +74,8 @@ func Close() error {
// Disable disables the logging. // Disable disables the logging.
func Disable() { func Disable() {
once.Do(func() { atomic.StoreUint32(&disableLog, 1)
atomic.StoreUint32(&initialized, 1) writer.Store(nopWriter{})
infoLog = iox.NopCloser(ioutil.Discard)
errorLog = iox.NopCloser(ioutil.Discard)
severeLog = iox.NopCloser(ioutil.Discard)
slowLog = iox.NopCloser(ioutil.Discard)
statLog = iox.NopCloser(ioutil.Discard)
stackLog = ioutil.Discard
})
} }
// DisableStat disables the stat logs. // DisableStat disables the stat logs.
@@ -224,22 +85,12 @@ func DisableStat() {
// Error writes v into error log. // Error writes v into error log.
func Error(v ...interface{}) { func Error(v ...interface{}) {
ErrorCaller(1, v...) errorTextSync(fmt.Sprint(v...))
}
// ErrorCaller writes v with context into error log.
func ErrorCaller(callDepth int, v ...interface{}) {
errorTextSync(fmt.Sprint(v...), callDepth+callerInnerDepth)
}
// ErrorCallerf writes v with context in format into error log.
func ErrorCallerf(callDepth int, format string, v ...interface{}) {
errorTextSync(fmt.Errorf(format, v...).Error(), callDepth+callerInnerDepth)
} }
// Errorf writes v with format into error log. // Errorf writes v with format into error log.
func Errorf(format string, v ...interface{}) { func Errorf(format string, v ...interface{}) {
ErrorCallerf(1, format, v...) errorTextSync(fmt.Errorf(format, v...).Error())
} }
// ErrorStack writes v along with call stack into error log. // ErrorStack writes v along with call stack into error log.
@@ -260,6 +111,49 @@ func Errorv(v interface{}) {
errorAnySync(v) errorAnySync(v)
} }
// Errorw writes msg along with fields into error log.
func Errorw(msg string, fields ...LogField) {
errorFieldsSync(msg, fields...)
}
// Field returns a LogField for the given key and value.
func Field(key string, value interface{}) LogField {
switch val := value.(type) {
case error:
return LogField{Key: key, Value: val.Error()}
case []error:
var errs []string
for _, err := range val {
errs = append(errs, err.Error())
}
return LogField{Key: key, Value: errs}
case time.Duration:
return LogField{Key: key, Value: fmt.Sprint(val)}
case []time.Duration:
var durs []string
for _, dur := range val {
durs = append(durs, fmt.Sprint(dur))
}
return LogField{Key: key, Value: durs}
case []time.Time:
var times []string
for _, t := range val {
times = append(times, fmt.Sprint(t))
}
return LogField{Key: key, Value: times}
case fmt.Stringer:
return LogField{Key: key, Value: val.String()}
case []fmt.Stringer:
var strs []string
for _, str := range val {
strs = append(strs, str.String())
}
return LogField{Key: key, Value: strs}
default:
return LogField{Key: key, Value: val}
}
}
// Info writes v into access log. // Info writes v into access log.
func Info(v ...interface{}) { func Info(v ...interface{}) {
infoTextSync(fmt.Sprint(v...)) infoTextSync(fmt.Sprint(v...))
@@ -275,14 +169,31 @@ func Infov(v interface{}) {
infoAnySync(v) infoAnySync(v)
} }
// Must checks if err is nil, otherwise logs the err and exits. // Infow writes msg along with fields into access log.
func Infow(msg string, fields ...LogField) {
infoFieldsSync(msg, fields...)
}
// Must checks if err is nil, otherwise logs the error and exits.
func Must(err error) { func Must(err error) {
if err != nil { if err == nil {
msg := formatWithCaller(err.Error(), 3) return
log.Print(msg)
outputText(severeLog, levelFatal, msg)
os.Exit(1)
} }
msg := err.Error()
log.Print(msg)
getWriter().Severe(msg)
os.Exit(1)
}
// MustSetup sets up logging with given config c. It exits on error.
func MustSetup(c LogConf) {
Must(SetUp(c))
}
// Reset clears the writer and resets the log level.
func Reset() Writer {
return writer.Swap(nil)
} }
// SetLevel sets the logging level. It can be used to suppress some logs. // SetLevel sets the logging level. It can be used to suppress some logs.
@@ -290,6 +201,47 @@ func SetLevel(level uint32) {
atomic.StoreUint32(&logLevel, level) atomic.StoreUint32(&logLevel, level)
} }
// SetWriter sets the logging writer. It can be used to customize the logging.
func SetWriter(w Writer) {
if atomic.LoadUint32(&disableLog) == 0 {
writer.Store(w)
}
}
// SetUp sets up the logx. If already set up, just return nil.
// we allow SetUp to be called multiple times, because for example
// we need to allow different service frameworks to initialize logx respectively.
func SetUp(c LogConf) (err error) {
// Just ignore the subsequent SetUp calls.
// Because multiple services in one process might call SetUp respectively.
// Need to wait for the first caller to complete the execution.
setupOnce.Do(func() {
setupLogLevel(c)
if len(c.TimeFormat) > 0 {
timeFormat = c.TimeFormat
}
switch c.Encoding {
case plainEncoding:
atomic.StoreUint32(&encoding, plainEncodingType)
default:
atomic.StoreUint32(&encoding, jsonEncodingType)
}
switch c.Mode {
case fileMode:
err = setupWithFiles(c)
case volumeMode:
err = setupWithVolume(c)
default:
setupWithConsole()
}
})
return
}
// Severe writes v into severe log. // Severe writes v into severe log.
func Severe(v ...interface{}) { func Severe(v ...interface{}) {
severeSync(fmt.Sprint(v...)) severeSync(fmt.Sprint(v...))
@@ -315,6 +267,11 @@ func Slowv(v interface{}) {
slowAnySync(v) slowAnySync(v)
} }
// Sloww writes msg along with fields into slow log.
func Sloww(msg string, fields ...LogField) {
slowFieldsSync(msg, fields...)
}
// Stat writes v into stat log. // Stat writes v into stat log.
func Stat(v ...interface{}) { func Stat(v ...interface{}) {
statSync(fmt.Sprint(v...)) statSync(fmt.Sprint(v...))
@@ -346,63 +303,67 @@ func WithGzip() LogOption {
} }
} }
// WithMaxBackups customizes how many log files backups will be kept.
func WithMaxBackups(count int) LogOption {
return func(opts *logOptions) {
opts.maxBackups = count
}
}
// WithMaxSize customizes how much space the writing log file can take up.
func WithMaxSize(size int) LogOption {
return func(opts *logOptions) {
opts.maxSize = size
}
}
// WithRotation customizes which log rotation rule to use.
func WithRotation(r string) LogOption {
return func(opts *logOptions) {
opts.rotationRule = r
}
}
func createOutput(path string) (io.WriteCloser, error) { func createOutput(path string) (io.WriteCloser, error) {
if len(path) == 0 { if len(path) == 0 {
return nil, ErrLogPathNotSet return nil, ErrLogPathNotSet
} }
return NewLogger(path, DefaultRotateRule(path, backupFileDelimiter, options.keepDays, switch options.rotationRule {
options.gzipEnabled), options.gzipEnabled) case sizeRotationRule:
return NewLogger(path, NewSizeLimitRotateRule(path, backupFileDelimiter, options.keepDays,
options.maxSize, options.maxBackups, options.gzipEnabled), options.gzipEnabled)
default:
return NewLogger(path, DefaultRotateRule(path, backupFileDelimiter, options.keepDays,
options.gzipEnabled), options.gzipEnabled)
}
} }
func errorAnySync(v interface{}) { func errorAnySync(v interface{}) {
if shallLog(ErrorLevel) { if shallLog(ErrorLevel) {
outputAny(errorLog, levelError, v) getWriter().Error(v)
} }
} }
func errorTextSync(msg string, callDepth int) { func errorFieldsSync(content string, fields ...LogField) {
if shallLog(ErrorLevel) { if shallLog(ErrorLevel) {
outputError(errorLog, msg, callDepth) getWriter().Error(content, fields...)
} }
} }
func formatWithCaller(msg string, callDepth int) string { func errorTextSync(msg string) {
var buf strings.Builder if shallLog(ErrorLevel) {
getWriter().Error(msg)
caller := getCaller(callDepth)
if len(caller) > 0 {
buf.WriteString(caller)
buf.WriteByte(' ')
} }
buf.WriteString(msg)
return buf.String()
} }
func getCaller(callDepth int) string { func getWriter() Writer {
var buf strings.Builder w := writer.Load()
if w == nil {
_, file, line, ok := runtime.Caller(callDepth) w = writer.StoreIfNil(newConsoleWriter())
if ok {
short := file
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
buf.WriteString(short)
buf.WriteByte(':')
buf.WriteString(strconv.Itoa(line))
} }
return buf.String() return w
}
func getTimestamp() string {
return timex.Time().Format(timeFormat)
} }
func handleOptions(opts []LogOption) { func handleOptions(opts []LogOption) {
@@ -413,56 +374,19 @@ func handleOptions(opts []LogOption) {
func infoAnySync(val interface{}) { func infoAnySync(val interface{}) {
if shallLog(InfoLevel) { if shallLog(InfoLevel) {
outputAny(infoLog, levelInfo, val) getWriter().Info(val)
}
}
func infoFieldsSync(content string, fields ...LogField) {
if shallLog(InfoLevel) {
getWriter().Info(content, fields...)
} }
} }
func infoTextSync(msg string) { func infoTextSync(msg string) {
if shallLog(InfoLevel) { if shallLog(InfoLevel) {
outputText(infoLog, levelInfo, msg) getWriter().Info(msg)
}
}
func outputAny(writer io.Writer, level string, val interface{}) {
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
writePlainAny(writer, level, val)
default:
info := logEntry{
Timestamp: getTimestamp(),
Level: level,
Content: val,
}
outputJson(writer, info)
}
}
func outputText(writer io.Writer, level, msg string) {
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
writePlainText(writer, level, msg)
default:
info := logEntry{
Timestamp: getTimestamp(),
Level: level,
Content: msg,
}
outputJson(writer, info)
}
}
func outputError(writer io.Writer, msg string, callDepth int) {
content := formatWithCaller(msg, callDepth)
outputText(writer, levelError, content)
}
func outputJson(writer io.Writer, info interface{}) {
if content, err := json.Marshal(info); err != nil {
log.Println(err.Error())
} else if atomic.LoadUint32(&initialized) == 0 || writer == nil {
log.Println(string(content))
} else {
writer.Write(append(content, '\n'))
} }
} }
@@ -477,72 +401,18 @@ func setupLogLevel(c LogConf) {
} }
} }
func setupWithConsole(c LogConf) { func setupWithConsole() {
once.Do(func() { SetWriter(newConsoleWriter())
atomic.StoreUint32(&initialized, 1)
writeConsole = true
setupLogLevel(c)
infoLog = newLogWriter(log.New(os.Stdout, "", flags))
errorLog = newLogWriter(log.New(os.Stderr, "", flags))
severeLog = newLogWriter(log.New(os.Stderr, "", flags))
slowLog = newLogWriter(log.New(os.Stderr, "", flags))
stackLog = newLessWriter(errorLog, options.logStackCooldownMills)
statLog = infoLog
})
} }
func setupWithFiles(c LogConf) error { func setupWithFiles(c LogConf) error {
var opts []LogOption w, err := newFileWriter(c)
var err error if err != nil {
return err
if len(c.Path) == 0 {
return ErrLogPathNotSet
} }
opts = append(opts, WithCooldownMillis(c.StackCooldownMillis)) SetWriter(w)
if c.Compress { return nil
opts = append(opts, WithGzip())
}
if c.KeepDays > 0 {
opts = append(opts, WithKeepDays(c.KeepDays))
}
accessFile := path.Join(c.Path, accessFilename)
errorFile := path.Join(c.Path, errorFilename)
severeFile := path.Join(c.Path, severeFilename)
slowFile := path.Join(c.Path, slowFilename)
statFile := path.Join(c.Path, statFilename)
once.Do(func() {
atomic.StoreUint32(&initialized, 1)
handleOptions(opts)
setupLogLevel(c)
if infoLog, err = createOutput(accessFile); err != nil {
return
}
if errorLog, err = createOutput(errorFile); err != nil {
return
}
if severeLog, err = createOutput(severeFile); err != nil {
return
}
if slowLog, err = createOutput(slowFile); err != nil {
return
}
if statLog, err = createOutput(statFile); err != nil {
return
}
stackLog = newLessWriter(errorLog, options.logStackCooldownMills)
})
return err
} }
func setupWithVolume(c LogConf) error { func setupWithVolume(c LogConf) error {
@@ -556,7 +426,7 @@ func setupWithVolume(c LogConf) error {
func severeSync(msg string) { func severeSync(msg string) {
if shallLog(SevereLevel) { if shallLog(SevereLevel) {
outputText(severeLog, levelSevere, fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
} }
} }
@@ -570,99 +440,30 @@ func shallLogStat() bool {
func slowAnySync(v interface{}) { func slowAnySync(v interface{}) {
if shallLog(ErrorLevel) { if shallLog(ErrorLevel) {
outputAny(slowLog, levelSlow, v) getWriter().Slow(v)
}
}
func slowFieldsSync(content string, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Slow(content, fields...)
} }
} }
func slowTextSync(msg string) { func slowTextSync(msg string) {
if shallLog(ErrorLevel) { if shallLog(ErrorLevel) {
outputText(slowLog, levelSlow, msg) getWriter().Slow(msg)
} }
} }
func stackSync(msg string) { func stackSync(msg string) {
if shallLog(ErrorLevel) { if shallLog(ErrorLevel) {
outputText(stackLog, levelError, fmt.Sprintf("%s\n%s", msg, string(debug.Stack()))) getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
} }
} }
func statSync(msg string) { func statSync(msg string) {
if shallLogStat() && shallLog(InfoLevel) { if shallLogStat() && shallLog(InfoLevel) {
outputText(statLog, levelStat, msg) getWriter().Stat(msg)
} }
} }
func writePlainAny(writer io.Writer, level string, val interface{}, fields ...string) {
switch v := val.(type) {
case string:
writePlainText(writer, level, v, fields...)
case error:
writePlainText(writer, level, v.Error(), fields...)
case fmt.Stringer:
writePlainText(writer, level, v.String(), fields...)
default:
var buf bytes.Buffer
buf.WriteString(getTimestamp())
buf.WriteByte(plainEncodingSep)
buf.WriteString(level)
for _, item := range fields {
buf.WriteByte(plainEncodingSep)
buf.WriteString(item)
}
buf.WriteByte(plainEncodingSep)
if err := json.NewEncoder(&buf).Encode(val); err != nil {
log.Println(err.Error())
return
}
buf.WriteByte('\n')
if atomic.LoadUint32(&initialized) == 0 || writer == nil {
log.Println(buf.String())
return
}
if _, err := writer.Write(buf.Bytes()); err != nil {
log.Println(err.Error())
}
}
}
func writePlainText(writer io.Writer, level, msg string, fields ...string) {
var buf bytes.Buffer
buf.WriteString(getTimestamp())
buf.WriteByte(plainEncodingSep)
buf.WriteString(level)
for _, item := range fields {
buf.WriteByte(plainEncodingSep)
buf.WriteString(item)
}
buf.WriteByte(plainEncodingSep)
buf.WriteString(msg)
buf.WriteByte('\n')
if atomic.LoadUint32(&initialized) == 0 || writer == nil {
log.Println(buf.String())
return
}
if _, err := writer.Write(buf.Bytes()); err != nil {
log.Println(err.Error())
}
}
type logWriter struct {
logger *log.Logger
}
func newLogWriter(logger *log.Logger) logWriter {
return logWriter{
logger: logger,
}
}
func (lw logWriter) Close() error {
return nil
}
func (lw logWriter) Write(data []byte) (int, error) {
lw.logger.Print(string(data))
return len(data), nil
}

View File

@@ -5,9 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"reflect"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
@@ -19,8 +19,9 @@ import (
) )
var ( var (
s = []byte("Sending #11 notification (id: 1451875113812010473) in #1 connection") s = []byte("Sending #11 notification (id: 1451875113812010473) in #1 connection")
pool = make(chan []byte, 1) pool = make(chan []byte, 1)
_ Writer = (*mockWriter)(nil)
) )
type mockWriter struct { type mockWriter struct {
@@ -28,10 +29,46 @@ type mockWriter struct {
builder strings.Builder builder strings.Builder
} }
func (mw *mockWriter) Write(data []byte) (int, error) { func (mw *mockWriter) Alert(v interface{}) {
mw.lock.Lock() mw.lock.Lock()
defer mw.lock.Unlock() defer mw.lock.Unlock()
return mw.builder.Write(data) output(&mw.builder, levelAlert, v)
}
func (mw *mockWriter) Error(v interface{}, fields ...LogField) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelError, v, fields...)
}
func (mw *mockWriter) Info(v interface{}, fields ...LogField) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelInfo, v, fields...)
}
func (mw *mockWriter) Severe(v interface{}) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelSevere, v)
}
func (mw *mockWriter) Slow(v interface{}, fields ...LogField) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelSlow, v, fields...)
}
func (mw *mockWriter) Stack(v interface{}) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelError, v)
}
func (mw *mockWriter) Stat(v interface{}, fields ...LogField) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelStat, v, fields...)
} }
func (mw *mockWriter) Close() error { func (mw *mockWriter) Close() error {
@@ -56,95 +93,211 @@ func (mw *mockWriter) String() string {
return mw.builder.String() return mw.builder.String()
} }
func TestField(t *testing.T) {
tests := []struct {
name string
f LogField
want map[string]interface{}
}{
{
name: "error",
f: Field("foo", errors.New("bar")),
want: map[string]interface{}{
"foo": "bar",
},
},
{
name: "errors",
f: Field("foo", []error{errors.New("bar"), errors.New("baz")}),
want: map[string]interface{}{
"foo": []interface{}{"bar", "baz"},
},
},
{
name: "strings",
f: Field("foo", []string{"bar", "baz"}),
want: map[string]interface{}{
"foo": []interface{}{"bar", "baz"},
},
},
{
name: "duration",
f: Field("foo", time.Second),
want: map[string]interface{}{
"foo": "1s",
},
},
{
name: "durations",
f: Field("foo", []time.Duration{time.Second, 2 * time.Second}),
want: map[string]interface{}{
"foo": []interface{}{"1s", "2s"},
},
},
{
name: "times",
f: Field("foo", []time.Time{
time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC),
time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC),
}),
want: map[string]interface{}{
"foo": []interface{}{"2020-01-01 00:00:00 +0000 UTC", "2020-01-02 00:00:00 +0000 UTC"},
},
},
{
name: "stringer",
f: Field("foo", ValStringer{val: "bar"}),
want: map[string]interface{}{
"foo": "bar",
},
},
{
name: "stringers",
f: Field("foo", []fmt.Stringer{ValStringer{val: "bar"}, ValStringer{val: "baz"}}),
want: map[string]interface{}{
"foo": []interface{}{"bar", "baz"},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
Infow("foo", test.f)
validateFields(t, w.String(), test.want)
})
}
}
func TestFileLineFileMode(t *testing.T) { func TestFileLineFileMode(t *testing.T) {
writer := new(mockWriter) w := new(mockWriter)
errorLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
file, line := getFileLine() file, line := getFileLine()
Error("anything") Error("anything")
assert.True(t, writer.Contains(fmt.Sprintf("%s:%d", file, line+1))) assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
writer.Reset()
file, line = getFileLine() file, line = getFileLine()
Errorf("anything %s", "format") Errorf("anything %s", "format")
assert.True(t, writer.Contains(fmt.Sprintf("%s:%d", file, line+1))) assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
} }
func TestFileLineConsoleMode(t *testing.T) { func TestFileLineConsoleMode(t *testing.T) {
writer := new(mockWriter) w := new(mockWriter)
writeConsole = true old := writer.Swap(w)
errorLog = newLogWriter(log.New(writer, "[ERROR] ", flags)) defer writer.Store(old)
atomic.StoreUint32(&initialized, 1)
file, line := getFileLine() file, line := getFileLine()
Error("anything") Error("anything")
assert.True(t, writer.Contains(fmt.Sprintf("%s:%d", file, line+1))) assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
writer.Reset() w.Reset()
file, line = getFileLine() file, line = getFileLine()
Errorf("anything %s", "format") Errorf("anything %s", "format")
assert.True(t, writer.Contains(fmt.Sprintf("%s:%d", file, line+1))) assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
} }
func TestStructedLogAlert(t *testing.T) { func TestStructedLogAlert(t *testing.T) {
doTestStructedLog(t, levelAlert, func(writer io.WriteCloser) { w := new(mockWriter)
errorLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelAlert, w, func(v ...interface{}) {
Alert(fmt.Sprint(v...)) Alert(fmt.Sprint(v...))
}) })
} }
func TestStructedLogError(t *testing.T) { func TestStructedLogError(t *testing.T) {
doTestStructedLog(t, levelError, func(writer io.WriteCloser) { w := new(mockWriter)
errorLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
Error(v...) Error(v...)
}) })
} }
func TestStructedLogErrorf(t *testing.T) { func TestStructedLogErrorf(t *testing.T) {
doTestStructedLog(t, levelError, func(writer io.WriteCloser) { w := new(mockWriter)
errorLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
Errorf("%s", fmt.Sprint(v...)) Errorf("%s", fmt.Sprint(v...))
}) })
} }
func TestStructedLogErrorv(t *testing.T) { func TestStructedLogErrorv(t *testing.T) {
doTestStructedLog(t, levelError, func(writer io.WriteCloser) { w := new(mockWriter)
errorLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
Errorv(fmt.Sprint(v...)) Errorv(fmt.Sprint(v...))
}) })
} }
func TestStructedLogErrorw(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
Errorw(fmt.Sprint(v...), Field("foo", "bar"))
})
}
func TestStructedLogInfo(t *testing.T) { func TestStructedLogInfo(t *testing.T) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
Info(v...) Info(v...)
}) })
} }
func TestStructedLogInfof(t *testing.T) { func TestStructedLogInfof(t *testing.T) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
Infof("%s", fmt.Sprint(v...)) Infof("%s", fmt.Sprint(v...))
}) })
} }
func TestStructedLogInfov(t *testing.T) { func TestStructedLogInfov(t *testing.T) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
Infov(fmt.Sprint(v...)) Infov(fmt.Sprint(v...))
}) })
} }
func TestStructedLogInfow(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
Infow(fmt.Sprint(v...), Field("foo", "bar"))
})
}
func TestStructedLogInfoConsoleAny(t *testing.T) { func TestStructedLogInfoConsoleAny(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding) old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType) atomic.StoreUint32(&encoding, plainEncodingType)
defer func() { defer func() {
@@ -156,9 +309,11 @@ func TestStructedLogInfoConsoleAny(t *testing.T) {
} }
func TestStructedLogInfoConsoleAnyString(t *testing.T) { func TestStructedLogInfoConsoleAnyString(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding) old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType) atomic.StoreUint32(&encoding, plainEncodingType)
defer func() { defer func() {
@@ -170,9 +325,11 @@ func TestStructedLogInfoConsoleAnyString(t *testing.T) {
} }
func TestStructedLogInfoConsoleAnyError(t *testing.T) { func TestStructedLogInfoConsoleAnyError(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding) old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType) atomic.StoreUint32(&encoding, plainEncodingType)
defer func() { defer func() {
@@ -184,9 +341,11 @@ func TestStructedLogInfoConsoleAnyError(t *testing.T) {
} }
func TestStructedLogInfoConsoleAnyStringer(t *testing.T) { func TestStructedLogInfoConsoleAnyStringer(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding) old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType) atomic.StoreUint32(&encoding, plainEncodingType)
defer func() { defer func() {
@@ -200,9 +359,11 @@ func TestStructedLogInfoConsoleAnyStringer(t *testing.T) {
} }
func TestStructedLogInfoConsoleText(t *testing.T) { func TestStructedLogInfoConsoleText(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) { w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding) old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType) atomic.StoreUint32(&encoding, plainEncodingType)
defer func() { defer func() {
@@ -214,69 +375,94 @@ func TestStructedLogInfoConsoleText(t *testing.T) {
} }
func TestStructedLogSlow(t *testing.T) { func TestStructedLogSlow(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) { w := new(mockWriter)
slowLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
Slow(v...) Slow(v...)
}) })
} }
func TestStructedLogSlowf(t *testing.T) { func TestStructedLogSlowf(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) { w := new(mockWriter)
slowLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
Slowf(fmt.Sprint(v...)) Slowf(fmt.Sprint(v...))
}) })
} }
func TestStructedLogSlowv(t *testing.T) { func TestStructedLogSlowv(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) { w := new(mockWriter)
slowLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
Slowv(fmt.Sprint(v...)) Slowv(fmt.Sprint(v...))
}) })
} }
func TestStructedLogSloww(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
Sloww(fmt.Sprint(v...), Field("foo", time.Second))
})
}
func TestStructedLogStat(t *testing.T) { func TestStructedLogStat(t *testing.T) {
doTestStructedLog(t, levelStat, func(writer io.WriteCloser) { w := new(mockWriter)
statLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelStat, w, func(v ...interface{}) {
Stat(v...) Stat(v...)
}) })
} }
func TestStructedLogStatf(t *testing.T) { func TestStructedLogStatf(t *testing.T) {
doTestStructedLog(t, levelStat, func(writer io.WriteCloser) { w := new(mockWriter)
statLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelStat, w, func(v ...interface{}) {
Statf(fmt.Sprint(v...)) Statf(fmt.Sprint(v...))
}) })
} }
func TestStructedLogSevere(t *testing.T) { func TestStructedLogSevere(t *testing.T) {
doTestStructedLog(t, levelSevere, func(writer io.WriteCloser) { w := new(mockWriter)
severeLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelSevere, w, func(v ...interface{}) {
Severe(v...) Severe(v...)
}) })
} }
func TestStructedLogSeveref(t *testing.T) { func TestStructedLogSeveref(t *testing.T) {
doTestStructedLog(t, levelSevere, func(writer io.WriteCloser) { w := new(mockWriter)
severeLog = writer old := writer.Swap(w)
}, func(v ...interface{}) { defer writer.Store(old)
doTestStructedLog(t, levelSevere, w, func(v ...interface{}) {
Severef(fmt.Sprint(v...)) Severef(fmt.Sprint(v...))
}) })
} }
func TestStructedLogWithDuration(t *testing.T) { func TestStructedLogWithDuration(t *testing.T) {
const message = "hello there" const message = "hello there"
writer := new(mockWriter) w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
WithDuration(time.Second).Info(message) WithDuration(time.Second).Info(message)
var entry logEntry var entry logEntry
if err := json.Unmarshal([]byte(writer.builder.String()), &entry); err != nil { if err := json.Unmarshal([]byte(w.String()), &entry); err != nil {
t.Error(err) t.Error(err)
} }
assert.Equal(t, levelInfo, entry.Level) assert.Equal(t, levelInfo, entry.Level)
@@ -287,11 +473,12 @@ func TestStructedLogWithDuration(t *testing.T) {
func TestSetLevel(t *testing.T) { func TestSetLevel(t *testing.T) {
SetLevel(ErrorLevel) SetLevel(ErrorLevel)
const message = "hello there" const message = "hello there"
writer := new(mockWriter) w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
Info(message) Info(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
} }
func TestSetLevelTwiceWithMode(t *testing.T) { func TestSetLevelTwiceWithMode(t *testing.T) {
@@ -300,29 +487,35 @@ func TestSetLevelTwiceWithMode(t *testing.T) {
"console", "console",
"volumn", "volumn",
} }
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
for _, mode := range testModes { for _, mode := range testModes {
testSetLevelTwiceWithMode(t, mode) testSetLevelTwiceWithMode(t, mode, w)
} }
} }
func TestSetLevelWithDuration(t *testing.T) { func TestSetLevelWithDuration(t *testing.T) {
SetLevel(ErrorLevel) SetLevel(ErrorLevel)
const message = "hello there" const message = "hello there"
writer := new(mockWriter) w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
WithDuration(time.Second).Info(message) WithDuration(time.Second).Info(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
} }
func TestErrorfWithWrappedError(t *testing.T) { func TestErrorfWithWrappedError(t *testing.T) {
SetLevel(ErrorLevel) SetLevel(ErrorLevel)
const message = "there" const message = "there"
writer := new(mockWriter) w := new(mockWriter)
errorLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
Errorf("hello %w", errors.New(message)) Errorf("hello %w", errors.New(message))
assert.True(t, strings.Contains(writer.builder.String(), "hello there")) assert.True(t, strings.Contains(w.String(), "hello there"))
} }
func TestMustNil(t *testing.T) { func TestMustNil(t *testing.T) {
@@ -330,6 +523,11 @@ func TestMustNil(t *testing.T) {
} }
func TestSetup(t *testing.T) { func TestSetup(t *testing.T) {
defer func() {
SetLevel(InfoLevel)
atomic.StoreUint32(&encoding, jsonEncodingType)
}()
MustSetup(LogConf{ MustSetup(LogConf{
ServiceName: "any", ServiceName: "any",
Mode: "console", Mode: "console",
@@ -344,6 +542,17 @@ func TestSetup(t *testing.T) {
Mode: "volume", Mode: "volume",
Path: os.TempDir(), Path: os.TempDir(),
}) })
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
TimeFormat: timeFormat,
})
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
Encoding: plainEncoding,
})
assert.NotNil(t, setupWithVolume(LogConf{})) assert.NotNil(t, setupWithVolume(LogConf{}))
assert.NotNil(t, setupWithFiles(LogConf{})) assert.NotNil(t, setupWithFiles(LogConf{}))
assert.Nil(t, setupWithFiles(LogConf{ assert.Nil(t, setupWithFiles(LogConf{
@@ -364,6 +573,8 @@ func TestSetup(t *testing.T) {
_, err := createOutput("") _, err := createOutput("")
assert.NotNil(t, err) assert.NotNil(t, err)
Disable() Disable()
SetLevel(InfoLevel)
atomic.StoreUint32(&encoding, jsonEncodingType)
} }
func TestDisable(t *testing.T) { func TestDisable(t *testing.T) {
@@ -373,7 +584,6 @@ func TestDisable(t *testing.T) {
WithKeepDays(1)(&opt) WithKeepDays(1)(&opt)
WithGzip()(&opt) WithGzip()(&opt)
assert.Nil(t, Close()) assert.Nil(t, Close())
writeConsole = false
assert.Nil(t, Close()) assert.Nil(t, Close())
} }
@@ -381,11 +591,22 @@ func TestDisableStat(t *testing.T) {
DisableStat() DisableStat()
const message = "hello there" const message = "hello there"
writer := new(mockWriter) w := new(mockWriter)
statLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
Stat(message) Stat(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
}
func TestSetWriter(t *testing.T) {
atomic.StoreUint32(&disableLog, 0)
Reset()
SetWriter(nopWriter{})
assert.NotNil(t, writer.Load())
assert.True(t, writer.Load() == nopWriter{})
mocked := new(mockWriter)
SetWriter(mocked)
assert.Equal(t, mocked, writer.Load())
} }
func TestWithGzip(t *testing.T) { func TestWithGzip(t *testing.T) {
@@ -428,7 +649,7 @@ func BenchmarkCopyByteSlice(b *testing.B) {
buf = make([]byte, len(s)) buf = make([]byte, len(s))
copy(buf, s) copy(buf, s)
} }
fmt.Fprint(ioutil.Discard, buf) fmt.Fprint(io.Discard, buf)
} }
func BenchmarkCopyOnWriteByteSlice(b *testing.B) { func BenchmarkCopyOnWriteByteSlice(b *testing.B) {
@@ -437,7 +658,7 @@ func BenchmarkCopyOnWriteByteSlice(b *testing.B) {
size := len(s) size := len(s)
buf = s[:size:size] buf = s[:size:size]
} }
fmt.Fprint(ioutil.Discard, buf) fmt.Fprint(io.Discard, buf)
} }
func BenchmarkCacheByteSlice(b *testing.B) { func BenchmarkCacheByteSlice(b *testing.B) {
@@ -451,7 +672,7 @@ func BenchmarkCacheByteSlice(b *testing.B) {
func BenchmarkLogs(b *testing.B) { func BenchmarkLogs(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
log.SetOutput(ioutil.Discard) log.SetOutput(io.Discard)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
Info(i) Info(i)
} }
@@ -487,15 +708,11 @@ func put(b []byte) {
} }
} }
func doTestStructedLog(t *testing.T, level string, setup func(writer io.WriteCloser), func doTestStructedLog(t *testing.T, level string, w *mockWriter, write func(...interface{})) {
write func(...interface{})) {
const message = "hello there" const message = "hello there"
writer := new(mockWriter)
setup(writer)
atomic.StoreUint32(&initialized, 1)
write(message) write(message)
var entry logEntry var entry logEntry
if err := json.Unmarshal([]byte(writer.builder.String()), &entry); err != nil { if err := json.Unmarshal([]byte(w.String()), &entry); err != nil {
t.Error(err) t.Error(err)
} }
assert.Equal(t, level, entry.Level) assert.Equal(t, level, entry.Level)
@@ -504,18 +721,14 @@ func doTestStructedLog(t *testing.T, level string, setup func(writer io.WriteClo
assert.True(t, strings.Contains(val, message)) assert.True(t, strings.Contains(val, message))
} }
func doTestStructedLogConsole(t *testing.T, setup func(writer io.WriteCloser), func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...interface{})) {
write func(...interface{})) {
const message = "hello there" const message = "hello there"
writer := new(mockWriter)
setup(writer)
atomic.StoreUint32(&initialized, 1)
write(message) write(message)
println(writer.String()) assert.True(t, strings.Contains(w.String(), message))
assert.True(t, strings.Contains(writer.String(), message))
} }
func testSetLevelTwiceWithMode(t *testing.T, mode string) { func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) {
writer.Store(nil)
SetUp(LogConf{ SetUp(LogConf{
Mode: mode, Mode: mode,
Level: "error", Level: "error",
@@ -527,17 +740,14 @@ func testSetLevelTwiceWithMode(t *testing.T, mode string) {
Path: "/dev/null", Path: "/dev/null",
}) })
const message = "hello there" const message = "hello there"
writer := new(mockWriter)
infoLog = writer
atomic.StoreUint32(&initialized, 1)
Info(message) Info(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
Infof(message) Infof(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
ErrorStack(message) ErrorStack(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
ErrorStackf(message) ErrorStackf(message)
assert.Equal(t, 0, writer.builder.Len()) assert.Equal(t, 0, w.builder.Len())
} }
type ValStringer struct { type ValStringer struct {
@@ -547,3 +757,18 @@ type ValStringer struct {
func (v ValStringer) String() string { func (v ValStringer) String() string {
return v.val return v.val
} }
func validateFields(t *testing.T, content string, fields map[string]interface{}) {
var m map[string]interface{}
if err := json.Unmarshal([]byte(content), &m); err != nil {
t.Error(err)
}
for k, v := range fields {
if reflect.TypeOf(v).Kind() == reflect.Slice {
assert.EqualValues(t, v, m[k])
} else {
assert.Equal(t, v, m[k], content)
}
}
}

22
core/logx/logwriter.go Normal file
View File

@@ -0,0 +1,22 @@
package logx
import "log"
type logWriter struct {
logger *log.Logger
}
func newLogWriter(logger *log.Logger) logWriter {
return logWriter{
logger: logger,
}
}
func (lw logWriter) Close() error {
return nil
}
func (lw logWriter) Write(data []byte) (int, error) {
lw.logger.Print(string(data))
return len(data), nil
}

206
core/logx/readme-cn.md Normal file
View File

@@ -0,0 +1,206 @@
<IMG align="right" width="150px" src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/go-zero.png">
# logx
[English](readme.md) | 简体中文
## logx 配置
```go
type LogConf struct {
ServiceName string `json:",optional"`
Mode string `json:",default=console,options=[console,file,volume]"`
Encoding string `json:",default=json,options=[json,plain]"`
TimeFormat string `json:",optional"`
Path string `json:",default=logs"`
Level string `json:",default=info,options=[info,error,severe]"`
Compress bool `json:",optional"`
KeepDays int `json:",optional"`
StackCooldownMillis int `json:",default=100"`
MaxBackups int `json:",default=0"`
MaxSize int `json:",default=0"`
Rotation string `json:",default=daily,options=[daily,size]"`
}
```
- `ServiceName`:设置服务名称,可选。在 `volume` 模式下,该名称用于生成日志文件。在 `rest/zrpc` 服务中,名称将被自动设置为 `rest``zrpc` 的名称。
- `Mode`:输出日志的模式,默认是 `console`
- `console` 模式将日志写到 `stdout/stderr`
- `file` 模式将日志写到 `Path` 指定目录的文件中
- `volume` 模式在 docker 中使用,将日志写入挂载的卷中
- `Encoding`: 指示如何对日志进行编码,默认是 `json`
- `json`模式以 json 格式写日志
- `plain`模式用纯文本写日志,并带有终端颜色显示
- `TimeFormat`:自定义时间格式,可选。默认是 `2006-01-02T15:04:05.000Z07:00`
- `Path`:设置日志路径,默认为 `logs`
- `Level`: 用于过滤日志的日志级别。默认为 `info`
- `info`,所有日志都被写入
- `error`, `info` 的日志被丢弃
- `severe`, `info``error` 日志被丢弃,只有 `severe` 日志被写入
- `Compress`: 是否压缩日志文件,只在 `file` 模式下工作
- `KeepDays`:日志文件被保留多少天,在给定的天数之后,过期的文件将被自动删除。对 `console` 模式没有影响
- `StackCooldownMillis`:多少毫秒后再次写入堆栈跟踪。用来避免堆栈跟踪日志过多
- `MaxBackups`: 多少个日志文件备份将被保存。0代表所有备份都被保存。当`Rotation`被设置为`size`时才会起作用。注意:`KeepDays`选项的优先级会比`MaxBackups`高,即使`MaxBackups`被设置为0当达到`KeepDays`上限时备份文件同样会被删除。
- `MaxSize`: 当前被写入的日志文件最大可占用多少空间。0代表没有上限。单位为`MB`。当`Rotation`被设置为`size`时才会起作用。
- `Rotation`: 日志轮转策略类型。默认为`daily`(按天轮转)。
- `daily` 按天轮转。
- `size` 按日志大小轮转。
## 打印日志方法
```go
type Logger interface {
// Error logs a message at error level.
Error(...interface{})
// Errorf logs a message at error level.
Errorf(string, ...interface{})
// Errorv logs a message at error level.
Errorv(interface{})
// Errorw logs a message at error level.
Errorw(string, ...LogField)
// Info logs a message at info level.
Info(...interface{})
// Infof logs a message at info level.
Infof(string, ...interface{})
// Infov logs a message at info level.
Infov(interface{})
// Infow logs a message at info level.
Infow(string, ...LogField)
// Slow logs a message at slow level.
Slow(...interface{})
// Slowf logs a message at slow level.
Slowf(string, ...interface{})
// Slowv logs a message at slow level.
Slowv(interface{})
// Sloww logs a message at slow level.
Sloww(string, ...LogField)
// WithContext returns a new logger with the given context.
WithContext(context.Context) Logger
// WithDuration returns a new logger with the given duration.
WithDuration(time.Duration) Logger
}
```
- `Error`, `Info`, `Slow`: 将任何类型的信息写进日志,使用 `fmt.Sprint(...)` 来转换为 `string`
- `Errorf`, `Infof`, `Slowf`: 将指定格式的信息写入日志
- `Errorv`, `Infov`, `Slowv`: 将任何类型的信息写入日志,用 `json marshal` 编码
- `Errorw`, `Infow`, `Sloww`: 写日志,并带上给定的 `key:value` 字段
- `WithContext`:将给定的 ctx 注入日志信息,例如用于记录 `trace-id``span-id`
- `WithDuration`: 将指定的时间写入日志信息中,字段名为 `duration`
## 与第三方日志库集成
- zap
- 实现:[https://github.com/zeromicro/zero-contrib/blob/main/logx/zapx/zap.go](https://github.com/zeromicro/zero-contrib/blob/main/logx/zapx/zap.go)
- 使用示例:[https://github.com/zeromicro/zero-examples/blob/main/logx/zaplog/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/zaplog/main.go)
- logrus
- 实现:[https://github.com/zeromicro/zero-contrib/blob/main/logx/logrusx/logrus.go](https://github.com/zeromicro/zero-contrib/blob/main/logx/logrusx/logrus.go)
- 使用示例:[https://github.com/zeromicro/zero-examples/blob/main/logx/logrus/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/logrus/main.go)
对于其它的日志库,请参考上面示例实现,并欢迎提交 `PR` 到 [https://github.com/zeromicro/zero-contrib](https://github.com/zeromicro/zero-contrib)
## 将日志写到指定的存储
`logx`定义了两个接口,方便自定义 `logx`,将日志写入任何存储。
- `logx.NewWriter(w io.Writer)`
- `logx.SetWriter(write logx.Writer)`
例如如果我们想把日志写进kafka而不是控制台或文件我们可以像下面这样做。
```go
type KafkaWriter struct {
Pusher *kq.Pusher
}
func NewKafkaWriter(pusher *kq.Pusher) *KafkaWriter {
return &KafkaWriter{
Pusher: pusher,
}
}
func (w *KafkaWriter) Write(p []byte) (n int, err error) {
// writing log with newlines, trim them.
if err := w.Pusher.Push(strings.TrimSpace(string(p))); err != nil {
return 0, err
}
return len(p), nil
}
func main() {
pusher := kq.NewPusher([]string{"localhost:9092"}, "go-zero")
defer pusher.Close()
writer := logx.NewWriter(NewKafkaWriter(pusher))
logx.SetWriter(writer)
// more code
}
```
完整代码:[https://github.com/zeromicro/zero-examples/blob/main/logx/tokafka/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/tokafka/main.go)
## 过滤敏感字段
如果我们需要防止 `password` 字段被记录下来,我们可以像下面这样实现。
```go
type (
Message struct {
Name string
Password string
Message string
}
SensitiveLogger struct {
logx.Writer
}
)
func NewSensitiveLogger(writer logx.Writer) *SensitiveLogger {
return &SensitiveLogger{
Writer: writer,
}
}
func (l *SensitiveLogger) Info(msg interface{}, fields ...logx.LogField) {
if m, ok := msg.(Message); ok {
l.Writer.Info(Message{
Name: m.Name,
Password: "******",
Message: m.Message,
}, fields...)
} else {
l.Writer.Info(msg, fields...)
}
}
func main() {
// setup logx to make sure originalWriter not nil,
// the injected writer is only for filtering, like a middleware.
originalWriter := logx.Reset()
writer := NewSensitiveLogger(originalWriter)
logx.SetWriter(writer)
logx.Infov(Message{
Name: "foo",
Password: "shouldNotAppear",
Message: "bar",
})
// more code
}
```
完整代码:[https://github.com/zeromicro/zero-examples/blob/main/logx/filterfields/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/filterfields/main.go)
## 更多示例
[https://github.com/zeromicro/zero-examples/tree/main/logx](https://github.com/zeromicro/zero-examples/tree/main/logx)
## Give a Star! ⭐
如果你正在使用或者觉得这个项目对你有帮助,请 **star** 支持,感谢!

205
core/logx/readme.md Normal file
View File

@@ -0,0 +1,205 @@
<img align="right" width="150px" src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/go-zero.png">
# logx
English | [简体中文](readme-cn.md)
## logx configurations
```go
type LogConf struct {
ServiceName string `json:",optional"`
Mode string `json:",default=console,options=[console,file,volume]"`
Encoding string `json:",default=json,options=[json,plain]"`
TimeFormat string `json:",optional"`
Path string `json:",default=logs"`
Level string `json:",default=info,options=[info,error,severe]"`
Compress bool `json:",optional"`
KeepDays int `json:",optional"`
StackCooldownMillis int `json:",default=100"`
MaxBackups int `json:",default=0"`
MaxSize int `json:",default=0"`
Rotation string `json:",default=daily,options=[daily,size]"`
}
```
- `ServiceName`: set the service name, optional. on `volume` mode, the name is used to generate the log files. Within `rest/zrpc` services, the name will be set to the name of `rest` or `zrpc` automatically.
- `Mode`: the mode to output the logs, default is `console`.
- `console` mode writes the logs to `stdout/stderr`.
- `file` mode writes the logs to the files specified by `Path`.
- `volume` mode is used in docker, to write logs into mounted volumes.
- `Encoding`: indicates how to encode the logs, default is `json`.
- `json` mode writes the logs in json format.
- `plain` mode writes the logs with plain text, with terminal color enabled.
- `TimeFormat`: customize the time format, optional. Default is `2006-01-02T15:04:05.000Z07:00`.
- `Path`: set the log path, default to `logs`.
- `Level`: the logging level to filter logs. Default is `info`.
- `info`, all logs are written.
- `error`, `info` logs are suppressed.
- `severe`, `info` and `error` logs are suppressed, only `severe` logs are written.
- `Compress`: whether or not to compress log files, only works with `file` mode.
- `KeepDays`: how many days that the log files are kept, after the given days, the outdated files will be deleted automatically. It has no effect on `console` mode.
- `StackCooldownMillis`: how many milliseconds to rewrite stacktrace again. Its used to avoid stacktrace flooding.
- `MaxBackups`: represents how many backup log files will be kept. 0 means all files will be kept forever. Only take effect when `Rotation` is `size`. NOTE: the level of option `KeepDays` will be higher. Even thougth `MaxBackups` sets 0, log files will still be removed if the `KeepDays` limitation is reached.
- `MaxSize`: represents how much space the writing log file takes up. 0 means no limit. The unit is `MB`. Only take effect when `Rotation` is `size`.
- `Rotation`: represents the type of log rotation rule. Default is `daily`.
- `daily` rotate the logs by day.
- `size` rotate the logs by size of logs.
## Logging methods
```go
type Logger interface {
// Error logs a message at error level.
Error(...interface{})
// Errorf logs a message at error level.
Errorf(string, ...interface{})
// Errorv logs a message at error level.
Errorv(interface{})
// Errorw logs a message at error level.
Errorw(string, ...LogField)
// Info logs a message at info level.
Info(...interface{})
// Infof logs a message at info level.
Infof(string, ...interface{})
// Infov logs a message at info level.
Infov(interface{})
// Infow logs a message at info level.
Infow(string, ...LogField)
// Slow logs a message at slow level.
Slow(...interface{})
// Slowf logs a message at slow level.
Slowf(string, ...interface{})
// Slowv logs a message at slow level.
Slowv(interface{})
// Sloww logs a message at slow level.
Sloww(string, ...LogField)
// WithContext returns a new logger with the given context.
WithContext(context.Context) Logger
// WithDuration returns a new logger with the given duration.
WithDuration(time.Duration) Logger
}
```
- `Error`, `Info`, `Slow`: write any kind of messages into logs, with like `fmt.Sprint(…)`.
- `Errorf`, `Infof`, `Slowf`: write messages with given format into logs.
- `Errorv`, `Infov`, `Slowv`: write any kind of messages into logs, with json marshalling to encode them.
- `Errorw`, `Infow`, `Sloww`: write the string message with given `key:value` fields.
- `WithContext`: inject the given ctx into the log messages, typically used to log `trace-id` and `span-id`.
- `WithDuration`: write elapsed duration into the log messages, with key `duration`.
## Integrating with third-party logging libs
- zap
- implementation: [https://github.com/zeromicro/zero-contrib/blob/main/logx/zapx/zap.go](https://github.com/zeromicro/zero-contrib/blob/main/logx/zapx/zap.go)
- usage example: [https://github.com/zeromicro/zero-examples/blob/main/logx/zaplog/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/zaplog/main.go)
- logrus
- implementation: [https://github.com/zeromicro/zero-contrib/blob/main/logx/logrusx/logrus.go](https://github.com/zeromicro/zero-contrib/blob/main/logx/logrusx/logrus.go)
- usage example: [https://github.com/zeromicro/zero-examples/blob/main/logx/logrus/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/logrus/main.go)
For more libs, please implement and PR to [https://github.com/zeromicro/zero-contrib](https://github.com/zeromicro/zero-contrib)
## Write the logs to specific stores
`logx` defined two interfaces to let you customize `logx` to write logs into any stores.
- `logx.NewWriter(w io.Writer)`
- `logx.SetWriter(writer logx.Writer)`
For example, if we want to write the logs into kafka instead of console or files, we can do it like below:
```go
type KafkaWriter struct {
Pusher *kq.Pusher
}
func NewKafkaWriter(pusher *kq.Pusher) *KafkaWriter {
return &KafkaWriter{
Pusher: pusher,
}
}
func (w *KafkaWriter) Write(p []byte) (n int, err error) {
// writing log with newlines, trim them.
if err := w.Pusher.Push(strings.TrimSpace(string(p))); err != nil {
return 0, err
}
return len(p), nil
}
func main() {
pusher := kq.NewPusher([]string{"localhost:9092"}, "go-zero")
defer pusher.Close()
writer := logx.NewWriter(NewKafkaWriter(pusher))
logx.SetWriter(writer)
// more code
}
```
Complete code: [https://github.com/zeromicro/zero-examples/blob/main/logx/tokafka/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/tokafka/main.go)
## Filtering sensitive fields
If we need to prevent the `password` fields from logging, we can do it like below:
```go
type (
Message struct {
Name string
Password string
Message string
}
SensitiveLogger struct {
logx.Writer
}
)
func NewSensitiveLogger(writer logx.Writer) *SensitiveLogger {
return &SensitiveLogger{
Writer: writer,
}
}
func (l *SensitiveLogger) Info(msg interface{}, fields ...logx.LogField) {
if m, ok := msg.(Message); ok {
l.Writer.Info(Message{
Name: m.Name,
Password: "******",
Message: m.Message,
}, fields...)
} else {
l.Writer.Info(msg, fields...)
}
}
func main() {
// setup logx to make sure originalWriter not nil,
// the injected writer is only for filtering, like a middleware.
originalWriter := logx.Reset()
writer := NewSensitiveLogger(originalWriter)
logx.SetWriter(writer)
logx.Infov(Message{
Name: "foo",
Password: "shouldNotAppear",
Message: "bar",
})
// more code
}
```
Complete code: [https://github.com/zeromicro/zero-examples/blob/main/logx/filterfields/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/filterfields/main.go)
## More examples
[https://github.com/zeromicro/zero-examples/tree/main/logx](https://github.com/zeromicro/zero-examples/tree/main/logx)
## Give a Star! ⭐
If you like or are using this project to learn or start your solution, please give it a star. Thanks!

View File

@@ -9,21 +9,24 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/zeromicro/go-zero/core/fs" "github.com/zeromicro/go-zero/core/fs"
"github.com/zeromicro/go-zero/core/lang" "github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/timex"
) )
const ( const (
dateFormat = "2006-01-02" dateFormat = "2006-01-02"
fileTimeFormat = time.RFC3339
hoursPerDay = 24 hoursPerDay = 24
bufferSize = 100 bufferSize = 100
defaultDirMode = 0o755 defaultDirMode = 0o755
defaultFileMode = 0o600 defaultFileMode = 0o600
gzipExt = ".gz"
megaBytes = 1 << 20
) )
// ErrLogFileClosed is an error that indicates the log file is already closed. // ErrLogFileClosed is an error that indicates the log file is already closed.
@@ -35,7 +38,7 @@ type (
BackupFileName() string BackupFileName() string
MarkRotated() MarkRotated()
OutdatedFiles() []string OutdatedFiles() []string
ShallRotate() bool ShallRotate(size int64) bool
} }
// A RotateLogger is a Logger that can rotate log files with given rules. // A RotateLogger is a Logger that can rotate log files with given rules.
@@ -48,8 +51,9 @@ type (
rule RotateRule rule RotateRule
compress bool compress bool
// can't use threading.RoutineGroup because of cycle import // can't use threading.RoutineGroup because of cycle import
waitGroup sync.WaitGroup waitGroup sync.WaitGroup
closeOnce sync.Once closeOnce sync.Once
currentSize int64
} }
// A DailyRotateRule is a rule to daily rotate the log files. // A DailyRotateRule is a rule to daily rotate the log files.
@@ -60,6 +64,13 @@ type (
days int days int
gzip bool gzip bool
} }
// SizeLimitRotateRule a rotation rule that make the log file rotated base on size
SizeLimitRotateRule struct {
DailyRotateRule
maxSize int64
maxBackups int
}
) )
// DefaultRotateRule is a default log rotating rule, currently DailyRotateRule. // DefaultRotateRule is a default log rotating rule, currently DailyRotateRule.
@@ -91,7 +102,7 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
var pattern string var pattern string
if r.gzip { if r.gzip {
pattern = fmt.Sprintf("%s%s*.gz", r.filename, r.delimiter) pattern = fmt.Sprintf("%s%s*%s", r.filename, r.delimiter, gzipExt)
} else { } else {
pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter) pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter)
} }
@@ -106,7 +117,7 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
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(dateFormat)
fmt.Fprintf(&buf, "%s%s%s", r.filename, r.delimiter, boundary) fmt.Fprintf(&buf, "%s%s%s", r.filename, r.delimiter, boundary)
if r.gzip { if r.gzip {
buf.WriteString(".gz") buf.WriteString(gzipExt)
} }
boundaryFile := buf.String() boundaryFile := buf.String()
@@ -121,10 +132,100 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
} }
// ShallRotate checks if the file should be rotated. // ShallRotate checks if the file should be rotated.
func (r *DailyRotateRule) ShallRotate() bool { func (r *DailyRotateRule) ShallRotate(_ int64) bool {
return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime
} }
// NewSizeLimitRotateRule returns the rotation rule with size limit
func NewSizeLimitRotateRule(filename, delimiter string, days, maxSize, maxBackups int, gzip bool) RotateRule {
return &SizeLimitRotateRule{
DailyRotateRule: DailyRotateRule{
rotatedTime: getNowDateInRFC3339Format(),
filename: filename,
delimiter: delimiter,
days: days,
gzip: gzip,
},
maxSize: int64(maxSize) * megaBytes,
maxBackups: maxBackups,
}
}
func (r *SizeLimitRotateRule) BackupFileName() string {
dir := filepath.Dir(r.filename)
prefix, ext := r.parseFilename()
timestamp := getNowDateInRFC3339Format()
return filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, timestamp, ext))
}
func (r *SizeLimitRotateRule) MarkRotated() {
r.rotatedTime = getNowDateInRFC3339Format()
}
func (r *SizeLimitRotateRule) OutdatedFiles() []string {
dir := filepath.Dir(r.filename)
prefix, ext := r.parseFilename()
var pattern string
if r.gzip {
pattern = fmt.Sprintf("%s%s%s%s*%s%s", dir, string(filepath.Separator),
prefix, r.delimiter, ext, gzipExt)
} else {
pattern = fmt.Sprintf("%s%s%s%s*%s", dir, string(filepath.Separator),
prefix, r.delimiter, ext)
}
files, err := filepath.Glob(pattern)
if err != nil {
Errorf("failed to delete outdated log files, error: %s", err)
return nil
}
sort.Strings(files)
outdated := make(map[string]lang.PlaceholderType)
// test if too many backups
if r.maxBackups > 0 && len(files) > r.maxBackups {
for _, f := range files[:len(files)-r.maxBackups] {
outdated[f] = lang.Placeholder
}
files = files[len(files)-r.maxBackups:]
}
// test if any too old backups
if r.days > 0 {
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(fileTimeFormat)
boundaryFile := filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, boundary, ext))
if r.gzip {
boundaryFile += gzipExt
}
for _, f := range files {
if f >= boundaryFile {
break
}
outdated[f] = lang.Placeholder
}
}
var result []string
for k := range outdated {
result = append(result, k)
}
return result
}
func (r *SizeLimitRotateRule) ShallRotate(size int64) bool {
return r.maxSize > 0 && r.maxSize < size
}
func (r *SizeLimitRotateRule) parseFilename() (prefix, ext string) {
logName := filepath.Base(r.filename)
ext = filepath.Ext(r.filename)
prefix = logName[:len(logName)-len(ext)]
return
}
// NewLogger returns a RotateLogger with given filename and rule, etc. // NewLogger returns a RotateLogger with given filename and rule, etc.
func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) { func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) {
l := &RotateLogger{ l := &RotateLogger{
@@ -211,6 +312,12 @@ func (l *RotateLogger) maybeCompressFile(file string) {
ErrorStack(r) ErrorStack(r)
} }
}() }()
if _, err := os.Stat(file); err != nil {
// file not exists or other error, ignore compression
return
}
compressLogFile(file) compressLogFile(file)
} }
@@ -277,25 +384,27 @@ func (l *RotateLogger) startWorker() {
} }
func (l *RotateLogger) write(v []byte) { func (l *RotateLogger) write(v []byte) {
if l.rule.ShallRotate() { if l.rule.ShallRotate(l.currentSize + int64(len(v))) {
if err := l.rotate(); err != nil { if err := l.rotate(); err != nil {
log.Println(err) log.Println(err)
} else { } else {
l.rule.MarkRotated() l.rule.MarkRotated()
l.currentSize = 0
} }
} }
if l.fp != nil { if l.fp != nil {
l.fp.Write(v) l.fp.Write(v)
l.currentSize += int64(len(v))
} }
} }
func compressLogFile(file string) { func compressLogFile(file string) {
start := timex.Now() start := time.Now()
Infof("compressing log file: %s", file) Infof("compressing log file: %s", file)
if err := gzipFile(file); err != nil { if err := gzipFile(file); err != nil {
Errorf("compress error: %s", err) Errorf("compress error: %s", err)
} else { } else {
Infof("compressed log file: %s, took %s", file, timex.Since(start)) Infof("compressed log file: %s, took %s", file, time.Since(start))
} }
} }
@@ -303,6 +412,10 @@ func getNowDate() string {
return time.Now().Format(dateFormat) return time.Now().Format(dateFormat)
} }
func getNowDateInRFC3339Format() string {
return time.Now().Format(fileTimeFormat)
}
func gzipFile(file string) error { func gzipFile(file string) error {
in, err := os.Open(file) in, err := os.Open(file)
if err != nil { if err != nil {
@@ -310,7 +423,7 @@ func gzipFile(file string) error {
} }
defer in.Close() defer in.Close()
out, err := os.Create(fmt.Sprintf("%s.gz", file)) out, err := os.Create(fmt.Sprintf("%s%s", file, gzipExt))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -29,7 +29,34 @@ func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
func TestDailyRotateRuleShallRotate(t *testing.T) { func TestDailyRotateRuleShallRotate(t *testing.T) {
var rule DailyRotateRule var rule DailyRotateRule
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(dateFormat) rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(dateFormat)
assert.True(t, rule.ShallRotate()) assert.True(t, rule.ShallRotate(0))
}
func TestSizeLimitRotateRuleMarkRotated(t *testing.T) {
var rule SizeLimitRotateRule
rule.MarkRotated()
assert.Equal(t, getNowDateInRFC3339Format(), rule.rotatedTime)
}
func TestSizeLimitRotateRuleOutdatedFiles(t *testing.T) {
var rule SizeLimitRotateRule
assert.Empty(t, rule.OutdatedFiles())
rule.days = 1
assert.Empty(t, rule.OutdatedFiles())
rule.gzip = true
assert.Empty(t, rule.OutdatedFiles())
rule.maxBackups = 0
assert.Empty(t, rule.OutdatedFiles())
}
func TestSizeLimitRotateRuleShallRotate(t *testing.T) {
var rule SizeLimitRotateRule
rule.rotatedTime = time.Now().Add(time.Hour * 24).Format(fileTimeFormat)
rule.maxSize = 0
assert.False(t, rule.ShallRotate(0))
rule.maxSize = 100
assert.False(t, rule.ShallRotate(0))
assert.True(t, rule.ShallRotate(101*megaBytes))
} }
func TestRotateLoggerClose(t *testing.T) { func TestRotateLoggerClose(t *testing.T) {
@@ -57,6 +84,12 @@ func TestRotateLoggerGetBackupFilename(t *testing.T) {
} }
func TestRotateLoggerMayCompressFile(t *testing.T) { func TestRotateLoggerMayCompressFile(t *testing.T) {
old := os.Stdout
os.Stdout = os.NewFile(0, os.DevNull)
defer func() {
os.Stdout = old
}()
filename, err := fs.TempFilenameWithText("foo") filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err) assert.Nil(t, err)
if len(filename) > 0 { if len(filename) > 0 {
@@ -70,15 +103,18 @@ func TestRotateLoggerMayCompressFile(t *testing.T) {
} }
func TestRotateLoggerMayCompressFileTrue(t *testing.T) { func TestRotateLoggerMayCompressFileTrue(t *testing.T) {
old := os.Stdout
os.Stdout = os.NewFile(0, os.DevNull)
defer func() {
os.Stdout = old
}()
filename, err := fs.TempFilenameWithText("foo") filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err) assert.Nil(t, err)
logger, err := NewLogger(filename, new(DailyRotateRule), true) logger, err := NewLogger(filename, new(DailyRotateRule), true)
assert.Nil(t, err) assert.Nil(t, err)
if len(filename) > 0 { if len(filename) > 0 {
defer func() { defer os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
os.Remove(filename)
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
} }
logger.maybeCompressFile(filename) logger.maybeCompressFile(filename)
_, err = os.Stat(filename) _, err = os.Stat(filename)
@@ -92,7 +128,6 @@ func TestRotateLoggerRotate(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
if len(filename) > 0 { if len(filename) > 0 {
defer func() { defer func() {
os.Remove(filename)
os.Remove(logger.getBackupFilename()) os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}() }()
@@ -102,6 +137,10 @@ func TestRotateLoggerRotate(t *testing.T) {
case *os.LinkError: case *os.LinkError:
// avoid rename error on docker container // avoid rename error on docker container
assert.Equal(t, syscall.EXDEV, v.Err) assert.Equal(t, syscall.EXDEV, v.Err)
case *os.PathError:
// ignore remove error for tests,
// files are cleaned in GitHub actions.
assert.Equal(t, "remove", v.Op)
default: default:
assert.Nil(t, err) assert.Nil(t, err)
} }
@@ -115,12 +154,177 @@ func TestRotateLoggerWrite(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
if len(filename) > 0 { if len(filename) > 0 {
defer func() { defer func() {
os.Remove(filename)
os.Remove(logger.getBackupFilename()) os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz") os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}() }()
} }
// the following write calls cannot be changed to Write, because of DATA RACE.
logger.write([]byte(`foo`)) logger.write([]byte(`foo`))
rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat) rule.rotatedTime = time.Now().Add(-time.Hour * 24).Format(dateFormat)
logger.write([]byte(`bar`)) logger.write([]byte(`bar`))
logger.Close()
logger.write([]byte(`baz`))
}
func TestLogWriterClose(t *testing.T) {
assert.Nil(t, newLogWriter(nil).Close())
}
func TestRotateLoggerWithSizeLimitRotateRuleClose(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(SizeLimitRotateRule), false)
assert.Nil(t, err)
assert.Nil(t, logger.Close())
}
func TestRotateLoggerGetBackupWithSizeLimitRotateRuleFilename(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(SizeLimitRotateRule), false)
assert.Nil(t, err)
assert.True(t, len(logger.getBackupFilename()) > 0)
logger.backup = ""
assert.True(t, len(logger.getBackupFilename()) > 0)
}
func TestRotateLoggerWithSizeLimitRotateRuleMayCompressFile(t *testing.T) {
old := os.Stdout
os.Stdout = os.NewFile(0, os.DevNull)
defer func() {
os.Stdout = old
}()
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filename)
}
logger, err := NewLogger(filename, new(SizeLimitRotateRule), false)
assert.Nil(t, err)
logger.maybeCompressFile(filename)
_, err = os.Stat(filename)
assert.Nil(t, err)
}
func TestRotateLoggerWithSizeLimitRotateRuleMayCompressFileTrue(t *testing.T) {
old := os.Stdout
os.Stdout = os.NewFile(0, os.DevNull)
defer func() {
os.Stdout = old
}()
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
logger, err := NewLogger(filename, new(SizeLimitRotateRule), true)
assert.Nil(t, err)
if len(filename) > 0 {
defer os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}
logger.maybeCompressFile(filename)
_, err = os.Stat(filename)
assert.NotNil(t, err)
}
func TestRotateLoggerWithSizeLimitRotateRuleRotate(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
logger, err := NewLogger(filename, new(SizeLimitRotateRule), true)
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
}
err = logger.rotate()
switch v := err.(type) {
case *os.LinkError:
// avoid rename error on docker container
assert.Equal(t, syscall.EXDEV, v.Err)
case *os.PathError:
// ignore remove error for tests,
// files are cleaned in GitHub actions.
assert.Equal(t, "remove", v.Op)
default:
assert.Nil(t, err)
}
}
func TestRotateLoggerWithSizeLimitRotateRuleWrite(t *testing.T) {
filename, err := fs.TempFilenameWithText("foo")
assert.Nil(t, err)
rule := new(SizeLimitRotateRule)
logger, err := NewLogger(filename, rule, true)
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
}
// 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)
logger.write([]byte(`bar`))
logger.Close()
logger.write([]byte(`baz`))
}
func BenchmarkRotateLogger(b *testing.B) {
filename := "./test.log"
filename2 := "./test2.log"
dailyRotateRuleLogger, err1 := NewLogger(
filename,
DefaultRotateRule(
filename,
backupFileDelimiter,
1,
true,
),
true,
)
if err1 != nil {
b.Logf("Failed to new daily rotate rule logger: %v", err1)
b.FailNow()
}
sizeLimitRotateRuleLogger, err2 := NewLogger(
filename2,
NewSizeLimitRotateRule(
filename,
backupFileDelimiter,
1,
100,
10,
true,
),
true,
)
if err2 != nil {
b.Logf("Failed to new size limit rotate rule logger: %v", err1)
b.FailNow()
}
defer func() {
dailyRotateRuleLogger.Close()
sizeLimitRotateRuleLogger.Close()
os.Remove(filename)
os.Remove(filename2)
}()
b.Run("daily rotate rule", func(b *testing.B) {
for i := 0; i < b.N; i++ {
dailyRotateRuleLogger.write([]byte("testing\ntesting\n"))
}
})
b.Run("size limit rotate rule", func(b *testing.B) {
for i := 0; i < b.N; i++ {
sizeLimitRotateRuleLogger.write([]byte("testing\ntesting\n"))
}
})
} }

View File

@@ -29,16 +29,16 @@ func TestRedirector(t *testing.T) {
} }
func captureOutput(f func()) string { func captureOutput(f func()) string {
writer := new(mockWriter) w := new(mockWriter)
infoLog = writer old := writer.Swap(w)
atomic.StoreUint32(&initialized, 1) defer writer.Store(old)
prevLevel := atomic.LoadUint32(&logLevel) prevLevel := atomic.LoadUint32(&logLevel)
SetLevel(InfoLevel) SetLevel(InfoLevel)
f() f()
SetLevel(prevLevel) SetLevel(prevLevel)
return writer.builder.String() return w.String()
} }
func getContent(jsonStr string) string { func getContent(jsonStr string) string {

View File

@@ -1,124 +0,0 @@
package logx
import (
"context"
"fmt"
"io"
"sync/atomic"
"time"
"github.com/zeromicro/go-zero/core/timex"
"go.opentelemetry.io/otel/trace"
)
type traceLogger struct {
logEntry
Trace string `json:"trace,omitempty"`
Span string `json:"span,omitempty"`
ctx context.Context
}
func (l *traceLogger) Error(v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(errorLog, levelError, formatWithCaller(fmt.Sprint(v...), durationCallerDepth))
}
}
func (l *traceLogger) Errorf(format string, v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(errorLog, levelError, formatWithCaller(fmt.Sprintf(format, v...), durationCallerDepth))
}
}
func (l *traceLogger) Errorv(v interface{}) {
if shallLog(ErrorLevel) {
l.write(errorLog, levelError, v)
}
}
func (l *traceLogger) Info(v ...interface{}) {
if shallLog(InfoLevel) {
l.write(infoLog, levelInfo, fmt.Sprint(v...))
}
}
func (l *traceLogger) Infof(format string, v ...interface{}) {
if shallLog(InfoLevel) {
l.write(infoLog, levelInfo, fmt.Sprintf(format, v...))
}
}
func (l *traceLogger) Infov(v interface{}) {
if shallLog(InfoLevel) {
l.write(infoLog, levelInfo, v)
}
}
func (l *traceLogger) Slow(v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(slowLog, levelSlow, fmt.Sprint(v...))
}
}
func (l *traceLogger) Slowf(format string, v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(slowLog, levelSlow, fmt.Sprintf(format, v...))
}
}
func (l *traceLogger) Slowv(v interface{}) {
if shallLog(ErrorLevel) {
l.write(slowLog, levelSlow, v)
}
}
func (l *traceLogger) WithDuration(duration time.Duration) Logger {
l.Duration = timex.ReprOfDuration(duration)
return l
}
func (l *traceLogger) write(writer io.Writer, level string, val interface{}) {
traceID := traceIdFromContext(l.ctx)
spanID := spanIdFromContext(l.ctx)
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
writePlainAny(writer, level, val, l.Duration, traceID, spanID)
default:
outputJson(writer, &traceLogger{
logEntry: logEntry{
Timestamp: getTimestamp(),
Level: level,
Duration: l.Duration,
Content: val,
},
Trace: traceID,
Span: spanID,
})
}
}
// WithContext sets ctx to log, for keeping tracing information.
func WithContext(ctx context.Context) Logger {
return &traceLogger{
ctx: ctx,
}
}
func spanIdFromContext(ctx context.Context) string {
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.HasSpanID() {
return spanCtx.SpanID().String()
}
return ""
}
func traceIdFromContext(ctx context.Context) string {
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.HasTraceID() {
return spanCtx.TraceID().String()
}
return ""
}

View File

@@ -1,154 +0,0 @@
package logx
import (
"context"
"log"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
const (
traceKey = "trace"
spanKey = "span"
)
func TestTraceLog(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
WithContext(ctx).(*traceLogger).write(&buf, levelInfo, testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceError(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
errorLog = newLogWriter(log.New(&buf, "", flags))
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Error(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Errorf(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Errorv(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceInfo(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
infoLog = newLogWriter(log.New(&buf, "", flags))
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Infov(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceInfoConsole(t *testing.T) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, jsonEncodingType)
defer func() {
atomic.StoreUint32(&encoding, old)
}()
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
infoLog = newLogWriter(log.New(&buf, "", flags))
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.True(t, strings.Contains(buf.String(), traceIdFromContext(ctx)))
assert.True(t, strings.Contains(buf.String(), spanIdFromContext(ctx)))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.True(t, strings.Contains(buf.String(), traceIdFromContext(ctx)))
assert.True(t, strings.Contains(buf.String(), spanIdFromContext(ctx)))
buf.Reset()
l.WithDuration(time.Second).Infov(testlog)
assert.True(t, strings.Contains(buf.String(), traceIdFromContext(ctx)))
assert.True(t, strings.Contains(buf.String(), spanIdFromContext(ctx)))
}
func TestTraceSlow(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
slowLog = newLogWriter(log.New(&buf, "", flags))
otp := otel.GetTracerProvider()
tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
otel.SetTracerProvider(tp)
defer otel.SetTracerProvider(otp)
ctx, _ := tp.Tracer("foo").Start(context.Background(), "bar")
l := WithContext(ctx).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Slow(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Slowf(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Slowv(testlog)
assert.True(t, strings.Contains(buf.String(), traceKey))
assert.True(t, strings.Contains(buf.String(), spanKey))
}
func TestTraceWithoutContext(t *testing.T) {
var buf mockWriter
atomic.StoreUint32(&initialized, 1)
infoLog = newLogWriter(log.New(&buf, "", flags))
l := WithContext(context.Background()).(*traceLogger)
SetLevel(InfoLevel)
l.WithDuration(time.Second).Info(testlog)
assert.False(t, strings.Contains(buf.String(), traceKey))
assert.False(t, strings.Contains(buf.String(), spanKey))
buf.Reset()
l.WithDuration(time.Second).Infof(testlog)
assert.False(t, strings.Contains(buf.String(), traceKey))
assert.False(t, strings.Contains(buf.String(), spanKey))
}

35
core/logx/util.go Normal file
View File

@@ -0,0 +1,35 @@
package logx
import (
"fmt"
"runtime"
"strings"
"time"
)
func getCaller(callDepth int) string {
_, file, line, ok := runtime.Caller(callDepth)
if !ok {
return ""
}
return prettyCaller(file, line)
}
func getTimestamp() string {
return time.Now().Format(timeFormat)
}
func prettyCaller(file string, line int) string {
idx := strings.LastIndexByte(file, '/')
if idx < 0 {
return fmt.Sprintf("%s:%d", file, line)
}
idx = strings.LastIndexByte(file[:idx], '/')
if idx < 0 {
return fmt.Sprintf("%s:%d", file, line)
}
return fmt.Sprintf("%s:%d", file[idx+1:], line)
}

72
core/logx/util_test.go Normal file
View File

@@ -0,0 +1,72 @@
package logx
import (
"path/filepath"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGetCaller(t *testing.T) {
_, file, _, _ := runtime.Caller(0)
assert.Contains(t, getCaller(1), filepath.Base(file))
assert.True(t, len(getCaller(1<<10)) == 0)
}
func TestGetTimestamp(t *testing.T) {
ts := getTimestamp()
tm, err := time.Parse(timeFormat, ts)
assert.Nil(t, err)
assert.True(t, time.Since(tm) < time.Minute)
}
func TestPrettyCaller(t *testing.T) {
tests := []struct {
name string
file string
line int
want string
}{
{
name: "regular",
file: "logx_test.go",
line: 123,
want: "logx_test.go:123",
},
{
name: "relative",
file: "adhoc/logx_test.go",
line: 123,
want: "adhoc/logx_test.go:123",
},
{
name: "long path",
file: "github.com/zeromicro/go-zero/core/logx/util_test.go",
line: 12,
want: "logx/util_test.go:12",
},
{
name: "local path",
file: "/Users/kevin/go-zero/core/logx/util_test.go",
line: 1234,
want: "logx/util_test.go:1234",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, prettyCaller(test.file, test.line))
})
}
}
func BenchmarkGetCaller(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
getCaller(1)
}
}

60
core/logx/vars.go Normal file
View File

@@ -0,0 +1,60 @@
package logx
import "errors"
const (
// InfoLevel logs everything
InfoLevel uint32 = iota
// ErrorLevel includes errors, slows, stacks
ErrorLevel
// SevereLevel only log severe messages
SevereLevel
)
const (
jsonEncodingType = iota
plainEncodingType
plainEncoding = "plain"
plainEncodingSep = '\t'
sizeRotationRule = "size"
)
const (
accessFilename = "access.log"
errorFilename = "error.log"
severeFilename = "severe.log"
slowFilename = "slow.log"
statFilename = "stat.log"
fileMode = "file"
volumeMode = "volume"
levelAlert = "alert"
levelInfo = "info"
levelError = "error"
levelSevere = "severe"
levelFatal = "fatal"
levelSlow = "slow"
levelStat = "stat"
backupFileDelimiter = "-"
flags = 0x0
)
const (
callerKey = "caller"
contentKey = "content"
durationKey = "duration"
levelKey = "level"
spanKey = "span"
timestampKey = "@timestamp"
traceKey = "trace"
)
var (
// ErrLogPathNotSet is an error that indicates the log path is not set.
ErrLogPathNotSet = errors.New("log path must be set")
// ErrLogServiceNameNotSet is an error that indicates that the service name is not set.
ErrLogServiceNameNotSet = errors.New("log service name must be set")
)

371
core/logx/writer.go Normal file
View File

@@ -0,0 +1,371 @@
package logx
import (
"encoding/json"
"fmt"
"io"
"log"
"path"
"strings"
"sync"
"sync/atomic"
fatihcolor "github.com/fatih/color"
"github.com/zeromicro/go-zero/core/color"
)
type (
Writer interface {
Alert(v interface{})
Close() error
Error(v interface{}, fields ...LogField)
Info(v interface{}, fields ...LogField)
Severe(v interface{})
Slow(v interface{}, fields ...LogField)
Stack(v interface{})
Stat(v interface{}, fields ...LogField)
}
atomicWriter struct {
writer Writer
lock sync.RWMutex
}
concreteWriter struct {
infoLog io.WriteCloser
errorLog io.WriteCloser
severeLog io.WriteCloser
slowLog io.WriteCloser
statLog io.WriteCloser
stackLog io.Writer
}
)
// NewWriter creates a new Writer with the given io.Writer.
func NewWriter(w io.Writer) Writer {
lw := newLogWriter(log.New(w, "", flags))
return &concreteWriter{
infoLog: lw,
errorLog: lw,
severeLog: lw,
slowLog: lw,
statLog: lw,
stackLog: lw,
}
}
func (w *atomicWriter) Load() Writer {
w.lock.RLock()
defer w.lock.RUnlock()
return w.writer
}
func (w *atomicWriter) Store(v Writer) {
w.lock.Lock()
defer w.lock.Unlock()
w.writer = v
}
func (w *atomicWriter) StoreIfNil(v Writer) Writer {
w.lock.Lock()
defer w.lock.Unlock()
if w.writer == nil {
w.writer = v
}
return w.writer
}
func (w *atomicWriter) Swap(v Writer) Writer {
w.lock.Lock()
defer w.lock.Unlock()
old := w.writer
w.writer = v
return old
}
func newConsoleWriter() Writer {
outLog := newLogWriter(log.New(fatihcolor.Output, "", flags))
errLog := newLogWriter(log.New(fatihcolor.Error, "", flags))
return &concreteWriter{
infoLog: outLog,
errorLog: errLog,
severeLog: errLog,
slowLog: errLog,
stackLog: newLessWriter(errLog, options.logStackCooldownMills),
statLog: outLog,
}
}
func newFileWriter(c LogConf) (Writer, error) {
var err error
var opts []LogOption
var infoLog io.WriteCloser
var errorLog io.WriteCloser
var severeLog io.WriteCloser
var slowLog io.WriteCloser
var statLog io.WriteCloser
var stackLog io.Writer
if len(c.Path) == 0 {
return nil, ErrLogPathNotSet
}
opts = append(opts, WithCooldownMillis(c.StackCooldownMillis))
if c.Compress {
opts = append(opts, WithGzip())
}
if c.KeepDays > 0 {
opts = append(opts, WithKeepDays(c.KeepDays))
}
if c.MaxBackups > 0 {
opts = append(opts, WithMaxBackups(c.MaxBackups))
}
if c.MaxSize > 0 {
opts = append(opts, WithMaxSize(c.MaxSize))
}
opts = append(opts, WithRotation(c.Rotation))
accessFile := path.Join(c.Path, accessFilename)
errorFile := path.Join(c.Path, errorFilename)
severeFile := path.Join(c.Path, severeFilename)
slowFile := path.Join(c.Path, slowFilename)
statFile := path.Join(c.Path, statFilename)
handleOptions(opts)
setupLogLevel(c)
if infoLog, err = createOutput(accessFile); err != nil {
return nil, err
}
if errorLog, err = createOutput(errorFile); err != nil {
return nil, err
}
if severeLog, err = createOutput(severeFile); err != nil {
return nil, err
}
if slowLog, err = createOutput(slowFile); err != nil {
return nil, err
}
if statLog, err = createOutput(statFile); err != nil {
return nil, err
}
stackLog = newLessWriter(errorLog, options.logStackCooldownMills)
return &concreteWriter{
infoLog: infoLog,
errorLog: errorLog,
severeLog: severeLog,
slowLog: slowLog,
statLog: statLog,
stackLog: stackLog,
}, nil
}
func (w *concreteWriter) Alert(v interface{}) {
output(w.errorLog, levelAlert, v)
}
func (w *concreteWriter) Close() error {
if err := w.infoLog.Close(); err != nil {
return err
}
if err := w.errorLog.Close(); err != nil {
return err
}
if err := w.severeLog.Close(); err != nil {
return err
}
if err := w.slowLog.Close(); err != nil {
return err
}
return w.statLog.Close()
}
func (w *concreteWriter) Error(v interface{}, fields ...LogField) {
output(w.errorLog, levelError, v, fields...)
}
func (w *concreteWriter) Info(v interface{}, fields ...LogField) {
output(w.infoLog, levelInfo, v, fields...)
}
func (w *concreteWriter) Severe(v interface{}) {
output(w.severeLog, levelFatal, v)
}
func (w *concreteWriter) Slow(v interface{}, fields ...LogField) {
output(w.slowLog, levelSlow, v, fields...)
}
func (w *concreteWriter) Stack(v interface{}) {
output(w.stackLog, levelError, v)
}
func (w *concreteWriter) Stat(v interface{}, fields ...LogField) {
output(w.statLog, levelStat, v, fields...)
}
type nopWriter struct{}
func (n nopWriter) Alert(_ interface{}) {
}
func (n nopWriter) Close() error {
return nil
}
func (n nopWriter) Error(_ interface{}, _ ...LogField) {
}
func (n nopWriter) Info(_ interface{}, _ ...LogField) {
}
func (n nopWriter) Severe(_ interface{}) {
}
func (n nopWriter) Slow(_ interface{}, _ ...LogField) {
}
func (n nopWriter) Stack(_ interface{}) {
}
func (n nopWriter) Stat(_ interface{}, _ ...LogField) {
}
func buildFields(fields ...LogField) []string {
var items []string
for _, field := range fields {
items = append(items, fmt.Sprintf("%s=%v", field.Key, field.Value))
}
return items
}
func output(writer io.Writer, level string, val interface{}, fields ...LogField) {
fields = append(fields, Field(callerKey, getCaller(callerDepth)))
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
writePlainAny(writer, level, val, buildFields(fields...)...)
default:
entry := make(logEntryWithFields)
for _, field := range fields {
entry[field.Key] = field.Value
}
entry[timestampKey] = getTimestamp()
entry[levelKey] = level
entry[contentKey] = val
writeJson(writer, entry)
}
}
func wrapLevelWithColor(level string) string {
var colour color.Color
switch level {
case levelAlert:
colour = color.FgRed
case levelError:
colour = color.FgRed
case levelFatal:
colour = color.FgRed
case levelInfo:
colour = color.FgBlue
case levelSlow:
colour = color.FgYellow
case levelStat:
colour = color.FgGreen
}
if colour == color.NoColor {
return level
}
return color.WithColorPadding(level, colour)
}
func writeJson(writer io.Writer, info interface{}) {
if content, err := json.Marshal(info); err != nil {
log.Println(err.Error())
} else if writer == nil {
log.Println(string(content))
} else {
writer.Write(append(content, '\n'))
}
}
func writePlainAny(writer io.Writer, level string, val interface{}, fields ...string) {
level = wrapLevelWithColor(level)
switch v := val.(type) {
case string:
writePlainText(writer, level, v, fields...)
case error:
writePlainText(writer, level, v.Error(), fields...)
case fmt.Stringer:
writePlainText(writer, level, v.String(), fields...)
default:
writePlainValue(writer, level, v, fields...)
}
}
func writePlainText(writer io.Writer, level, msg string, fields ...string) {
var buf strings.Builder
buf.WriteString(getTimestamp())
buf.WriteByte(plainEncodingSep)
buf.WriteString(level)
buf.WriteByte(plainEncodingSep)
buf.WriteString(msg)
for _, item := range fields {
buf.WriteByte(plainEncodingSep)
buf.WriteString(item)
}
buf.WriteByte('\n')
if writer == nil {
log.Println(buf.String())
return
}
if _, err := fmt.Fprint(writer, buf.String()); err != nil {
log.Println(err.Error())
}
}
func writePlainValue(writer io.Writer, level string, val interface{}, fields ...string) {
var buf strings.Builder
buf.WriteString(getTimestamp())
buf.WriteByte(plainEncodingSep)
buf.WriteString(level)
buf.WriteByte(plainEncodingSep)
if err := json.NewEncoder(&buf).Encode(val); err != nil {
log.Println(err.Error())
return
}
for _, item := range fields {
buf.WriteByte(plainEncodingSep)
buf.WriteString(item)
}
buf.WriteByte('\n')
if writer == nil {
log.Println(buf.String())
return
}
if _, err := fmt.Fprint(writer, buf.String()); err != nil {
log.Println(err.Error())
}
}

179
core/logx/writer_test.go Normal file
View File

@@ -0,0 +1,179 @@
package logx
import (
"bytes"
"encoding/json"
"errors"
"log"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewWriter(t *testing.T) {
const literal = "foo bar"
var buf bytes.Buffer
w := NewWriter(&buf)
w.Info(literal)
assert.Contains(t, buf.String(), literal)
}
func TestConsoleWriter(t *testing.T) {
var buf bytes.Buffer
w := newConsoleWriter()
lw := newLogWriter(log.New(&buf, "", 0))
w.(*concreteWriter).errorLog = lw
w.Alert("foo bar 1")
var val mockedEntry
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Fatal(err)
}
assert.Equal(t, levelAlert, val.Level)
assert.Equal(t, "foo bar 1", val.Content)
buf.Reset()
w.(*concreteWriter).errorLog = lw
w.Error("foo bar 2")
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Fatal(err)
}
assert.Equal(t, levelError, val.Level)
assert.Equal(t, "foo bar 2", val.Content)
buf.Reset()
w.(*concreteWriter).infoLog = lw
w.Info("foo bar 3")
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Fatal(err)
}
assert.Equal(t, levelInfo, val.Level)
assert.Equal(t, "foo bar 3", val.Content)
buf.Reset()
w.(*concreteWriter).severeLog = lw
w.Severe("foo bar 4")
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Fatal(err)
}
assert.Equal(t, levelFatal, val.Level)
assert.Equal(t, "foo bar 4", val.Content)
buf.Reset()
w.(*concreteWriter).slowLog = lw
w.Slow("foo bar 5")
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Fatal(err)
}
assert.Equal(t, levelSlow, val.Level)
assert.Equal(t, "foo bar 5", val.Content)
buf.Reset()
w.(*concreteWriter).statLog = lw
w.Stat("foo bar 6")
if err := json.Unmarshal(buf.Bytes(), &val); err != nil {
t.Fatal(err)
}
assert.Equal(t, levelStat, val.Level)
assert.Equal(t, "foo bar 6", val.Content)
w.(*concreteWriter).infoLog = hardToCloseWriter{}
assert.NotNil(t, w.Close())
w.(*concreteWriter).infoLog = easyToCloseWriter{}
w.(*concreteWriter).errorLog = hardToCloseWriter{}
assert.NotNil(t, w.Close())
w.(*concreteWriter).errorLog = easyToCloseWriter{}
w.(*concreteWriter).severeLog = hardToCloseWriter{}
assert.NotNil(t, w.Close())
w.(*concreteWriter).severeLog = easyToCloseWriter{}
w.(*concreteWriter).slowLog = hardToCloseWriter{}
assert.NotNil(t, w.Close())
w.(*concreteWriter).slowLog = easyToCloseWriter{}
w.(*concreteWriter).statLog = hardToCloseWriter{}
assert.NotNil(t, w.Close())
w.(*concreteWriter).statLog = easyToCloseWriter{}
}
func TestNopWriter(t *testing.T) {
assert.NotPanics(t, func() {
var w nopWriter
w.Alert("foo")
w.Error("foo")
w.Info("foo")
w.Severe("foo")
w.Stack("foo")
w.Stat("foo")
w.Slow("foo")
w.Close()
})
}
func TestWriteJson(t *testing.T) {
var buf bytes.Buffer
log.SetOutput(&buf)
writeJson(nil, "foo")
assert.Contains(t, buf.String(), "foo")
buf.Reset()
writeJson(nil, make(chan int))
assert.Contains(t, buf.String(), "unsupported type")
}
func TestWritePlainAny(t *testing.T) {
var buf bytes.Buffer
log.SetOutput(&buf)
writePlainAny(nil, levelInfo, "foo")
assert.Contains(t, buf.String(), "foo")
buf.Reset()
writePlainAny(nil, levelError, make(chan int))
assert.Contains(t, buf.String(), "unsupported type")
writePlainAny(nil, levelSlow, 100)
assert.Contains(t, buf.String(), "100")
buf.Reset()
writePlainAny(hardToWriteWriter{}, levelStat, 100)
assert.Contains(t, buf.String(), "write error")
buf.Reset()
writePlainAny(hardToWriteWriter{}, levelSevere, "foo")
assert.Contains(t, buf.String(), "write error")
buf.Reset()
writePlainAny(hardToWriteWriter{}, levelAlert, "foo")
assert.Contains(t, buf.String(), "write error")
buf.Reset()
writePlainAny(hardToWriteWriter{}, levelFatal, "foo")
assert.Contains(t, buf.String(), "write error")
}
type mockedEntry struct {
Level string `json:"level"`
Content string `json:"content"`
}
type easyToCloseWriter struct{}
func (h easyToCloseWriter) Write(_ []byte) (_ int, _ error) {
return
}
func (h easyToCloseWriter) Close() error {
return nil
}
type hardToCloseWriter struct{}
func (h hardToCloseWriter) Write(_ []byte) (_ int, _ error) {
return
}
func (h hardToCloseWriter) Close() error {
return errors.New("close error")
}
type hardToWriteWriter struct{}
func (h hardToWriteWriter) Write(_ []byte) (_ int, _ error) {
return 0, errors.New("write error")
}

186
core/mapping/marshaler.go Normal file
View File

@@ -0,0 +1,186 @@
package mapping
import (
"fmt"
"reflect"
"strings"
)
const (
emptyTag = ""
tagKVSeparator = ":"
)
// 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 common used.
func Marshal(val interface{}) (map[string]map[string]interface{}, error) {
ret := make(map[string]map[string]interface{})
tp := reflect.TypeOf(val)
if tp.Kind() == reflect.Ptr {
tp = tp.Elem()
}
rv := reflect.ValueOf(val)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i)
value := rv.Field(i)
if err := processMember(field, value, ret); err != nil {
return nil, err
}
}
return ret, nil
}
func getTag(field reflect.StructField) (string, bool) {
tag := string(field.Tag)
if i := strings.Index(tag, tagKVSeparator); i >= 0 {
return strings.TrimSpace(tag[:i]), true
}
return strings.TrimSpace(tag), false
}
func processMember(field reflect.StructField, value reflect.Value,
collector map[string]map[string]interface{}) error {
var key string
var opt *fieldOptions
var err error
tag, ok := getTag(field)
if !ok {
tag = emptyTag
key = field.Name
} else {
key, opt, err = parseKeyAndOptions(tag, field)
if err != nil {
return err
}
if err = validate(field, value, opt); err != nil {
return err
}
}
val := value.Interface()
if opt != nil && opt.FromString {
val = fmt.Sprint(val)
}
m, ok := collector[tag]
if ok {
m[key] = val
} else {
m = map[string]interface{}{
key: val,
}
}
collector[tag] = m
return nil
}
func validate(field reflect.StructField, value reflect.Value, opt *fieldOptions) error {
if opt == nil || !opt.Optional {
if err := validateOptional(field, value); err != nil {
return err
}
}
if opt == nil {
return nil
}
if opt.Optional && value.IsZero() {
return nil
}
if len(opt.Options) > 0 {
if err := validateOptions(value, opt); err != nil {
return err
}
}
if opt.Range != nil {
if err := validateRange(value, opt); err != nil {
return err
}
}
return nil
}
func validateOptional(field reflect.StructField, value reflect.Value) error {
switch field.Type.Kind() {
case reflect.Ptr:
if value.IsNil() {
return fmt.Errorf("field %q is nil", field.Name)
}
case reflect.Array, reflect.Slice, reflect.Map:
if value.IsNil() || value.Len() == 0 {
return fmt.Errorf("field %q is empty", field.Name)
}
}
return nil
}
func validateOptions(value reflect.Value, opt *fieldOptions) error {
var found bool
val := fmt.Sprint(value.Interface())
for i := range opt.Options {
if opt.Options[i] == val {
found = true
break
}
}
if !found {
return fmt.Errorf("field %q not in options", val)
}
return nil
}
func validateRange(value reflect.Value, opt *fieldOptions) error {
var val float64
switch v := value.Interface().(type) {
case int:
val = float64(v)
case int8:
val = float64(v)
case int16:
val = float64(v)
case int32:
val = float64(v)
case int64:
val = float64(v)
case uint:
val = float64(v)
case uint8:
val = float64(v)
case uint16:
val = float64(v)
case uint32:
val = float64(v)
case uint64:
val = float64(v)
case float32:
val = float64(v)
case float64:
val = v
default:
return fmt.Errorf("unknown support type for range %q", value.Type().String())
}
// validates [left, right], [left, right), (left, right], (left, right)
if val < opt.Range.left ||
(!opt.Range.leftInclude && val == opt.Range.left) ||
val > opt.Range.right ||
(!opt.Range.rightInclude && val == opt.Range.right) {
return fmt.Errorf("%v out of range", value.Interface())
}
return nil
}

View File

@@ -0,0 +1,346 @@
package mapping
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMarshal(t *testing.T) {
v := struct {
Name string `path:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
Anonymous bool
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
Anonymous: true,
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "kevin", m["path"]["name"])
assert.Equal(t, "shanghai", m["json"]["address"])
assert.Equal(t, 20, m["json"]["age"].(int))
assert.True(t, m[emptyTag]["Anonymous"].(bool))
}
func TestMarshal_Ptr(t *testing.T) {
v := &struct {
Name string `path:"name"`
Address string `json:"address,options=[beijing,shanghai]"`
Age int `json:"age"`
Anonymous bool
}{
Name: "kevin",
Address: "shanghai",
Age: 20,
Anonymous: true,
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "kevin", m["path"]["name"])
assert.Equal(t, "shanghai", m["json"]["address"])
assert.Equal(t, 20, m["json"]["age"].(int))
assert.True(t, m[emptyTag]["Anonymous"].(bool))
}
func TestMarshal_OptionalPtr(t *testing.T) {
var val = 1
v := struct {
Age *int `json:"age"`
}{
Age: &val,
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, 1, *m["json"]["age"].(*int))
}
func TestMarshal_OptionalPtrNil(t *testing.T) {
v := struct {
Age *int `json:"age"`
}{}
_, err := Marshal(v)
assert.NotNil(t, err)
}
func TestMarshal_BadOptions(t *testing.T) {
v := struct {
Name string `json:"name,options"`
}{
Name: "kevin",
}
_, err := Marshal(v)
assert.NotNil(t, err)
}
func TestMarshal_NotInOptions(t *testing.T) {
v := struct {
Name string `json:"name,options=[a,b]"`
}{
Name: "kevin",
}
_, err := Marshal(v)
assert.NotNil(t, err)
}
func TestMarshal_NotInOptionsOptional(t *testing.T) {
v := struct {
Name string `json:"name,options=[a,b],optional"`
}{}
_, err := Marshal(v)
assert.Nil(t, err)
}
func TestMarshal_NotInOptionsOptionalWrongValue(t *testing.T) {
v := struct {
Name string `json:"name,options=[a,b],optional"`
}{
Name: "kevin",
}
_, err := Marshal(v)
assert.NotNil(t, err)
}
func TestMarshal_Nested(t *testing.T) {
type address struct {
Country string `json:"country"`
City string `json:"city"`
}
v := struct {
Name string `json:"name,options=[kevin,wan]"`
Address address `json:"address"`
}{
Name: "kevin",
Address: address{
Country: "China",
City: "Shanghai",
},
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "kevin", m["json"]["name"])
assert.Equal(t, "China", m["json"]["address"].(address).Country)
assert.Equal(t, "Shanghai", m["json"]["address"].(address).City)
}
func TestMarshal_NestedPtr(t *testing.T) {
type address struct {
Country string `json:"country"`
City string `json:"city"`
}
v := struct {
Name string `json:"name,options=[kevin,wan]"`
Address *address `json:"address"`
}{
Name: "kevin",
Address: &address{
Country: "China",
City: "Shanghai",
},
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "kevin", m["json"]["name"])
assert.Equal(t, "China", m["json"]["address"].(*address).Country)
assert.Equal(t, "Shanghai", m["json"]["address"].(*address).City)
}
func TestMarshal_Slice(t *testing.T) {
v := struct {
Name []string `json:"name"`
}{
Name: []string{"kevin", "wan"},
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"kevin", "wan"}, m["json"]["name"].([]string))
}
func TestMarshal_SliceNil(t *testing.T) {
v := struct {
Name []string `json:"name"`
}{
Name: nil,
}
_, err := Marshal(v)
assert.NotNil(t, err)
}
func TestMarshal_Range(t *testing.T) {
v := struct {
Int int `json:"int,range=[1:3]"`
Int8 int8 `json:"int8,range=[1:3)"`
Int16 int16 `json:"int16,range=(1:3]"`
Int32 int32 `json:"int32,range=(1:3)"`
Int64 int64 `json:"int64,range=(1:3)"`
Uint uint `json:"uint,range=[1:3]"`
Uint8 uint8 `json:"uint8,range=[1:3)"`
Uint16 uint16 `json:"uint16,range=(1:3]"`
Uint32 uint32 `json:"uint32,range=(1:3)"`
Uint64 uint64 `json:"uint64,range=(1:3)"`
Float32 float32 `json:"float32,range=(1:3)"`
Float64 float64 `json:"float64,range=(1:3)"`
}{
Int: 1,
Int8: 1,
Int16: 2,
Int32: 2,
Int64: 2,
Uint: 1,
Uint8: 1,
Uint16: 2,
Uint32: 2,
Uint64: 2,
Float32: 2,
Float64: 2,
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, 1, m["json"]["int"].(int))
assert.Equal(t, int8(1), m["json"]["int8"].(int8))
assert.Equal(t, int16(2), m["json"]["int16"].(int16))
assert.Equal(t, int32(2), m["json"]["int32"].(int32))
assert.Equal(t, int64(2), m["json"]["int64"].(int64))
assert.Equal(t, uint(1), m["json"]["uint"].(uint))
assert.Equal(t, uint8(1), m["json"]["uint8"].(uint8))
assert.Equal(t, uint16(2), m["json"]["uint16"].(uint16))
assert.Equal(t, uint32(2), m["json"]["uint32"].(uint32))
assert.Equal(t, uint64(2), m["json"]["uint64"].(uint64))
assert.Equal(t, float32(2), m["json"]["float32"].(float32))
assert.Equal(t, float64(2), m["json"]["float64"].(float64))
}
func TestMarshal_RangeOut(t *testing.T) {
tests := []interface{}{
struct {
Int int `json:"int,range=[1:3]"`
}{
Int: 4,
},
struct {
Int int `json:"int,range=(1:3]"`
}{
Int: 1,
},
struct {
Int int `json:"int,range=[1:3)"`
}{
Int: 3,
},
struct {
Int int `json:"int,range=(1:3)"`
}{
Int: 3,
},
struct {
Bool bool `json:"bool,range=(1:3)"`
}{
Bool: true,
},
}
for _, test := range tests {
_, err := Marshal(test)
assert.NotNil(t, err)
}
}
func TestMarshal_RangeIllegal(t *testing.T) {
tests := []interface{}{
struct {
Int int `json:"int,range=[3:1]"`
}{
Int: 2,
},
struct {
Int int `json:"int,range=(3:1]"`
}{
Int: 2,
},
}
for _, test := range tests {
_, err := Marshal(test)
assert.Equal(t, err, errNumberRange)
}
}
func TestMarshal_RangeLeftEqualsToRight(t *testing.T) {
tests := []struct {
name string
value interface{}
err error
}{
{
name: "left inclusive, right inclusive",
value: struct {
Int int `json:"int,range=[2:2]"`
}{
Int: 2,
},
},
{
name: "left inclusive, right exclusive",
value: struct {
Int int `json:"int,range=[2:2)"`
}{
Int: 2,
},
err: errNumberRange,
},
{
name: "left exclusive, right inclusive",
value: struct {
Int int `json:"int,range=(2:2]"`
}{
Int: 2,
},
err: errNumberRange,
},
{
name: "left exclusive, right exclusive",
value: struct {
Int int `json:"int,range=(2:2)"`
}{
Int: 2,
},
err: errNumberRange,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
_, err := Marshal(test.value)
assert.Equal(t, test.err, err)
})
}
}
func TestMarshal_FromString(t *testing.T) {
v := struct {
Age int `json:"age,string"`
}{
Age: 10,
}
m, err := Marshal(v)
assert.Nil(t, err)
assert.Equal(t, "10", m["json"]["age"].(string))
}

View File

@@ -0,0 +1,29 @@
package mapping
import (
"bytes"
"encoding/json"
"io"
"github.com/pelletier/go-toml/v2"
)
// UnmarshalTomlBytes unmarshals TOML bytes into the given v.
func UnmarshalTomlBytes(content []byte, v interface{}) error {
return UnmarshalTomlReader(bytes.NewReader(content), v)
}
// UnmarshalTomlReader unmarshals TOML from the given io.Reader into the given v.
func UnmarshalTomlReader(r io.Reader, v interface{}) error {
var val interface{}
if err := toml.NewDecoder(r).Decode(&val); err != nil {
return err
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(val); err != nil {
return err
}
return UnmarshalJsonReader(&buf, v)
}

View File

@@ -0,0 +1,41 @@
package mapping
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnmarshalToml(t *testing.T) {
const input = `a = "foo"
b = 1
c = "${FOO}"
d = "abcd!@#$112"
`
var val struct {
A string `json:"a"`
B int `json:"b"`
C string `json:"c"`
D string `json:"d"`
}
assert.Nil(t, UnmarshalTomlBytes([]byte(input), &val))
assert.Equal(t, "foo", val.A)
assert.Equal(t, 1, val.B)
assert.Equal(t, "${FOO}", val.C)
assert.Equal(t, "abcd!@#$112", val.D)
}
func TestUnmarshalTomlErrorToml(t *testing.T) {
const input = `foo"
b = 1
c = "${FOO}"
d = "abcd!@#$112"
`
var val struct {
A string `json:"a"`
B int `json:"b"`
C string `json:"c"`
D string `json:"d"`
}
assert.NotNil(t, UnmarshalTomlBytes([]byte(input), &val))
}

View File

@@ -97,10 +97,6 @@ func (u *Unmarshaler) unmarshalWithFullName(m Valuer, v interface{}, fullName st
numFields := rte.NumField() numFields := rte.NumField()
for i := 0; i < numFields; i++ { for i := 0; i < numFields; i++ {
field := rte.Field(i) field := rte.Field(i)
if usingDifferentKeys(u.key, field) {
continue
}
if err := u.processField(field, rve.Field(i), m, fullName); err != nil { if err := u.processField(field, rve.Field(i), m, fullName); err != nil {
return err return err
} }
@@ -235,7 +231,7 @@ func (u *Unmarshaler) processFieldPrimitive(field reflect.StructField, value ref
return u.processFieldPrimitiveWithJSONNumber(field, value, v, opts, fullName) return u.processFieldPrimitiveWithJSONNumber(field, value, v, opts, fullName)
default: default:
if typeKind == valueKind { if typeKind == valueKind {
if err := validateValueInOptions(opts.options(), mapValue); err != nil { if err := validateValueInOptions(mapValue, opts.options()); err != nil {
return err return err
} }
@@ -257,6 +253,10 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(field reflect.StructFi
return err return err
} }
if err := validateValueInOptions(v, opts.options()); err != nil {
return err
}
if fieldKind == reflect.Ptr { if fieldKind == reflect.Ptr {
value = value.Elem() value = value.Elem()
} }
@@ -275,6 +275,10 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(field reflect.StructFi
return err return err
} }
if iValue < 0 {
return fmt.Errorf("unmarshal %q with bad value %q", fullName, v.String())
}
value.SetUint(uint64(iValue)) value.SetUint(uint64(iValue))
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
fValue, err := v.Float64() fValue, err := v.Float64()
@@ -496,10 +500,20 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
return nil return nil
} }
func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value, mapValue interface{}) error { func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value,
mapValue interface{}) error {
var slice []interface{} var slice []interface{}
if err := jsonx.UnmarshalFromString(mapValue.(string), &slice); err != nil { switch v := mapValue.(type) {
return err case fmt.Stringer:
if err := jsonx.UnmarshalFromString(v.String(), &slice); err != nil {
return err
}
case string:
if err := jsonx.UnmarshalFromString(v, &slice); err != nil {
return err
}
default:
return errUnsupportedType
} }
baseFieldType := Deref(fieldType.Elem()) baseFieldType := Deref(fieldType.Elem())
@@ -520,8 +534,10 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
baseKind reflect.Kind, value interface{}) error { baseKind reflect.Kind, value interface{}) error {
ithVal := slice.Index(index) ithVal := slice.Index(index)
switch v := value.(type) { switch v := value.(type) {
case json.Number: case fmt.Stringer:
return setValue(baseKind, ithVal, v.String()) return setValue(baseKind, ithVal, v.String())
case string:
return setValue(baseKind, ithVal, v)
default: default:
// don't need to consider the difference between int, int8, int16, int32, int64, // 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. // uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
@@ -731,10 +747,10 @@ func fillWithSameType(field reflect.StructField, value reflect.Value, mapValue i
if field.Type.Kind() == reflect.Ptr { if field.Type.Kind() == reflect.Ptr {
baseType := Deref(field.Type) baseType := Deref(field.Type)
target := reflect.New(baseType).Elem() target := reflect.New(baseType).Elem()
target.Set(reflect.ValueOf(mapValue)) setSameKindValue(baseType, target, mapValue)
value.Set(target.Addr()) value.Set(target.Addr())
} else { } else {
value.Set(reflect.ValueOf(mapValue)) setSameKindValue(field.Type, value, mapValue)
} }
return nil return nil
@@ -809,3 +825,11 @@ func readKeys(key string) []string {
return keys return keys
} }
func setSameKindValue(targetType reflect.Type, target reflect.Value, value interface{}) {
if reflect.ValueOf(value).Type().AssignableTo(targetType) {
target.Set(reflect.ValueOf(value))
} else {
target.Set(reflect.ValueOf(value).Convert(targetType))
}
}

View File

@@ -987,6 +987,43 @@ func TestUnmarshalWithStringOptionsCorrect(t *testing.T) {
ast.Equal("2", in.Correct) ast.Equal("2", in.Correct)
} }
func TestUnmarshalOptionsOptional(t *testing.T) {
type inner struct {
Value string `key:"value,options=first|second,optional"`
OptionalValue string `key:"optional_value,options=first|second,optional"`
Foo string `key:"foo,options=[bar,baz]"`
Correct string `key:"correct,options=1|2"`
}
m := map[string]interface{}{
"value": "first",
"foo": "bar",
"correct": "2",
}
var in inner
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &in))
ast.Equal("first", in.Value)
ast.Equal("", in.OptionalValue)
ast.Equal("bar", in.Foo)
ast.Equal("2", in.Correct)
}
func TestUnmarshalOptionsOptionalWrongValue(t *testing.T) {
type inner struct {
Value string `key:"value,options=first|second,optional"`
OptionalValue string `key:"optional_value,options=first|second,optional"`
WrongValue string `key:"wrong_value,options=first|second,optional"`
}
m := map[string]interface{}{
"value": "first",
"wrong_value": "third",
}
var in inner
assert.NotNil(t, UnmarshalKey(m, &in))
}
func TestUnmarshalStringOptionsWithStringOptionsNotString(t *testing.T) { func TestUnmarshalStringOptionsWithStringOptionsNotString(t *testing.T) {
type inner struct { type inner struct {
Value string `key:"value,options=first|second"` Value string `key:"value,options=first|second"`
@@ -1133,6 +1170,28 @@ func TestUnmarshalWithIntOptionsIncorrect(t *testing.T) {
assert.NotNil(t, UnmarshalKey(m, &in)) assert.NotNil(t, UnmarshalKey(m, &in))
} }
func TestUnmarshalWithJsonNumberOptionsIncorrect(t *testing.T) {
type inner struct {
Value string `key:"value,options=first|second"`
Incorrect int `key:"incorrect,options=1|2"`
}
m := map[string]interface{}{
"value": "first",
"incorrect": json.Number("3"),
}
var in inner
assert.NotNil(t, UnmarshalKey(m, &in))
}
func TestUnmarshaler_UnmarshalIntOptions(t *testing.T) {
var val struct {
Sex int `json:"sex,options=0|1"`
}
input := []byte(`{"sex": 2}`)
assert.NotNil(t, UnmarshalJsonBytes(input, &val))
}
func TestUnmarshalWithUintOptionsCorrect(t *testing.T) { func TestUnmarshalWithUintOptionsCorrect(t *testing.T) {
type inner struct { type inner struct {
Value string `key:"value,options=first|second"` Value string `key:"value,options=first|second"`
@@ -2622,7 +2681,7 @@ func TestUnmarshalJsonReaderMultiArray(t *testing.T) {
assert.Equal(t, 2, len(res.B)) assert.Equal(t, 2, len(res.B))
} }
func TestUnmarshalJsonReaderPtrMultiArray(t *testing.T) { func TestUnmarshalJsonReaderPtrMultiArrayString(t *testing.T) {
payload := `{"a": "133", "b": [["add", "cccd"], ["eeee"]]}` payload := `{"a": "133", "b": [["add", "cccd"], ["eeee"]]}`
var res struct { var res struct {
A string `json:"a"` A string `json:"a"`
@@ -2635,6 +2694,32 @@ func TestUnmarshalJsonReaderPtrMultiArray(t *testing.T) {
assert.Equal(t, 2, len(res.B[0])) assert.Equal(t, 2, len(res.B[0]))
} }
func TestUnmarshalJsonReaderPtrMultiArrayString_Int(t *testing.T) {
payload := `{"a": "133", "b": [[11, 22], [33]]}`
var res struct {
A string `json:"a"`
B [][]*string `json:"b"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, 2, len(res.B))
assert.Equal(t, 2, len(res.B[0]))
}
func TestUnmarshalJsonReaderPtrMultiArrayInt(t *testing.T) {
payload := `{"a": "133", "b": [[11, 22], [33]]}`
var res struct {
A string `json:"a"`
B [][]*int `json:"b"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, 2, len(res.B))
assert.Equal(t, 2, len(res.B[0]))
}
func TestUnmarshalJsonReaderPtrArray(t *testing.T) { func TestUnmarshalJsonReaderPtrArray(t *testing.T) {
payload := `{"a": "133", "b": ["add", "cccd", "eeee"]}` payload := `{"a": "133", "b": ["add", "cccd", "eeee"]}`
var res struct { var res struct {
@@ -2647,6 +2732,30 @@ func TestUnmarshalJsonReaderPtrArray(t *testing.T) {
assert.Equal(t, 3, len(res.B)) assert.Equal(t, 3, len(res.B))
} }
func TestUnmarshalJsonReaderPtrArray_Int(t *testing.T) {
payload := `{"a": "133", "b": [11, 22, 33]}`
var res struct {
A string `json:"a"`
B []*string `json:"b"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, 3, len(res.B))
}
func TestUnmarshalJsonReaderPtrInt(t *testing.T) {
payload := `{"a": "133", "b": [11, 22, 33]}`
var res struct {
A string `json:"a"`
B []*string `json:"b"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, 3, len(res.B))
}
func TestUnmarshalJsonWithoutKey(t *testing.T) { func TestUnmarshalJsonWithoutKey(t *testing.T) {
payload := `{"A": "1", "B": "2"}` payload := `{"A": "1", "B": "2"}`
var res struct { var res struct {
@@ -2660,6 +2769,116 @@ func TestUnmarshalJsonWithoutKey(t *testing.T) {
assert.Equal(t, "2", res.B) assert.Equal(t, "2", res.B)
} }
func TestUnmarshalJsonUintNegative(t *testing.T) {
payload := `{"a": -1}`
var res struct {
A uint `json:"a"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.NotNil(t, err)
}
func TestUnmarshalJsonDefinedInt(t *testing.T) {
type Int int
var res struct {
A Int `json:"a"`
}
payload := `{"a": -1}`
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, Int(-1), res.A)
}
func TestUnmarshalJsonDefinedString(t *testing.T) {
type String string
var res struct {
A String `json:"a"`
}
payload := `{"a": "foo"}`
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, String("foo"), res.A)
}
func TestUnmarshalJsonDefinedStringPtr(t *testing.T) {
type String string
var res struct {
A *String `json:"a"`
}
payload := `{"a": "foo"}`
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.Nil(t, err)
assert.Equal(t, String("foo"), *res.A)
}
func TestUnmarshalJsonReaderComplex(t *testing.T) {
type (
MyInt int
MyTxt string
MyTxtArray []string
Req struct {
MyInt MyInt `json:"my_int"` // int.. ok
MyTxtArray MyTxtArray `json:"my_txt_array"`
MyTxt MyTxt `json:"my_txt"` // but string is not assignable
Int int `json:"int"`
Txt string `json:"txt"`
}
)
body := `{
"my_int": 100,
"my_txt_array": [
"a",
"b"
],
"my_txt": "my_txt",
"int": 200,
"txt": "txt"
}`
var req Req
err := UnmarshalJsonReader(strings.NewReader(body), &req)
assert.Nil(t, err)
assert.Equal(t, MyInt(100), req.MyInt)
assert.Equal(t, MyTxt("my_txt"), req.MyTxt)
assert.EqualValues(t, MyTxtArray([]string{"a", "b"}), req.MyTxtArray)
assert.Equal(t, 200, req.Int)
assert.Equal(t, "txt", req.Txt)
}
func TestUnmarshalJsonReaderArrayBool(t *testing.T) {
payload := `{"id": false}`
var res struct {
ID []string `json:"id"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.NotNil(t, err)
}
func TestUnmarshalJsonReaderArrayInt(t *testing.T) {
payload := `{"id": 123}`
var res struct {
ID []string `json:"id"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.NotNil(t, err)
}
func TestUnmarshalJsonReaderArrayString(t *testing.T) {
payload := `{"id": "123"}`
var res struct {
ID []string `json:"id"`
}
reader := strings.NewReader(payload)
err := UnmarshalJsonReader(reader, &res)
assert.NotNil(t, err)
}
func BenchmarkDefaultValue(b *testing.B) { func BenchmarkDefaultValue(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
var a struct { var a struct {

View File

@@ -143,6 +143,23 @@ func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fie
return key, &fieldOpts, nil return key, &fieldOpts, nil
} }
// ensureValue ensures nested members not to be nil.
// If pointer value is nil, set to a new value.
func ensureValue(v reflect.Value) reflect.Value {
for {
if v.Kind() != reflect.Ptr {
break
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
v = v.Elem()
}
return v
}
func implicitValueRequiredStruct(tag string, tp reflect.Type) (bool, error) { func implicitValueRequiredStruct(tag string, tp reflect.Type) (bool, error) {
numFields := tp.NumField() numFields := tp.NumField()
for i := 0; i < numFields; i++ { for i := 0; i < numFields; i++ {
@@ -294,6 +311,20 @@ func parseNumberRange(str string) (*numberRange, error) {
right = math.MaxFloat64 right = math.MaxFloat64
} }
if left > right {
return nil, errNumberRange
}
// [2:2] valid
// [2:2) invalid
// (2:2] invalid
// (2:2) invalid
if left == right {
if !leftInclude || !rightInclude {
return nil, errNumberRange
}
}
return &numberRange{ return &numberRange{
left: left, left: left,
leftInclude: leftInclude, leftInclude: leftInclude,
@@ -478,6 +509,7 @@ func setValue(kind reflect.Kind, value reflect.Value, str string) error {
return errValueNotSettable return errValueNotSettable
} }
value = ensureValue(value)
v, err := convertType(kind, str) v, err := convertType(kind, str)
if err != nil { if err != nil {
return err return err
@@ -592,16 +624,16 @@ func validateNumberRange(fv float64, nr *numberRange) error {
return nil return nil
} }
func validateValueInOptions(options []string, value interface{}) error { func validateValueInOptions(val interface{}, options []string) error {
if len(options) > 0 { if len(options) > 0 {
switch v := value.(type) { switch v := val.(type) {
case string: case string:
if !stringx.Contains(options, v) { if !stringx.Contains(options, v) {
return fmt.Errorf(`error: value "%s" is not defined in options "%v"`, v, options) return fmt.Errorf(`error: value "%s" is not defined in options "%v"`, v, options)
} }
default: default:
if !stringx.Contains(options, Repr(v)) { if !stringx.Contains(options, Repr(v)) {
return fmt.Errorf(`error: value "%v" is not defined in options "%v"`, value, options) return fmt.Errorf(`error: value "%v" is not defined in options "%v"`, val, options)
} }
} }
} }

View File

@@ -15,7 +15,7 @@ type Foo struct {
StrWithTagAndOption string `key:"stringwithtag,string"` StrWithTagAndOption string `key:"stringwithtag,string"`
} }
func TestDeferInt(t *testing.T) { func TestDerefInt(t *testing.T) {
i := 1 i := 1
s := "hello" s := "hello"
number := struct { number := struct {
@@ -60,6 +60,51 @@ func TestDeferInt(t *testing.T) {
} }
} }
func TestDerefValInt(t *testing.T) {
i := 1
s := "hello"
number := struct {
f float64
}{
f: 6.4,
}
cases := []struct {
t reflect.Value
expect reflect.Kind
}{
{
t: reflect.ValueOf(i),
expect: reflect.Int,
},
{
t: reflect.ValueOf(&i),
expect: reflect.Int,
},
{
t: reflect.ValueOf(s),
expect: reflect.String,
},
{
t: reflect.ValueOf(&s),
expect: reflect.String,
},
{
t: reflect.ValueOf(number.f),
expect: reflect.Float64,
},
{
t: reflect.ValueOf(&number.f),
expect: reflect.Float64,
},
}
for _, each := range cases {
t.Run(each.t.String(), func(t *testing.T) {
assert.Equal(t, each.expect, ensureValue(each.t).Kind())
})
}
}
func TestParseKeyAndOptionWithoutTag(t *testing.T) { func TestParseKeyAndOptionWithoutTag(t *testing.T) {
var foo Foo var foo Foo
rte := reflect.TypeOf(&foo).Elem() rte := reflect.TypeOf(&foo).Elem()

View File

@@ -3,6 +3,7 @@ package metric
import ( import (
prom "github.com/prometheus/client_golang/prometheus" prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc" "github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
) )
type ( type (
@@ -47,10 +48,18 @@ func NewCounterVec(cfg *CounterVecOpts) CounterVec {
} }
func (cv *promCounterVec) Inc(labels ...string) { func (cv *promCounterVec) Inc(labels ...string) {
if !prometheus.Enabled() {
return
}
cv.counter.WithLabelValues(labels...).Inc() cv.counter.WithLabelValues(labels...).Inc()
} }
func (cv *promCounterVec) Add(v float64, labels ...string) { func (cv *promCounterVec) Add(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
cv.counter.WithLabelValues(labels...).Add(v) cv.counter.WithLabelValues(labels...).Add(v)
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil" "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/prometheus"
) )
func TestNewCounterVec(t *testing.T) { func TestNewCounterVec(t *testing.T) {
@@ -21,6 +22,7 @@ func TestNewCounterVec(t *testing.T) {
} }
func TestCounterIncr(t *testing.T) { func TestCounterIncr(t *testing.T) {
startAgent()
counterVec := NewCounterVec(&CounterVecOpts{ counterVec := NewCounterVec(&CounterVecOpts{
Namespace: "http_client", Namespace: "http_client",
Subsystem: "call", Subsystem: "call",
@@ -37,6 +39,7 @@ func TestCounterIncr(t *testing.T) {
} }
func TestCounterAdd(t *testing.T) { func TestCounterAdd(t *testing.T) {
startAgent()
counterVec := NewCounterVec(&CounterVecOpts{ counterVec := NewCounterVec(&CounterVecOpts{
Namespace: "rpc_server", Namespace: "rpc_server",
Subsystem: "requests", Subsystem: "requests",
@@ -51,3 +54,11 @@ func TestCounterAdd(t *testing.T) {
r := testutil.ToFloat64(cv.counter) r := testutil.ToFloat64(cv.counter)
assert.Equal(t, float64(33), r) assert.Equal(t, float64(33), r)
} }
func startAgent() {
prometheus.StartAgent(prometheus.Config{
Host: "127.0.0.1",
Port: 9101,
Path: "/metrics",
})
}

View File

@@ -3,6 +3,7 @@ package metric
import ( import (
prom "github.com/prometheus/client_golang/prometheus" prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc" "github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
) )
type ( type (
@@ -50,14 +51,26 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
} }
func (gv *promGaugeVec) Inc(labels ...string) { func (gv *promGaugeVec) Inc(labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Inc() gv.gauge.WithLabelValues(labels...).Inc()
} }
func (gv *promGaugeVec) Add(v float64, labels ...string) { func (gv *promGaugeVec) Add(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Add(v) gv.gauge.WithLabelValues(labels...).Add(v)
} }
func (gv *promGaugeVec) Set(v float64, labels ...string) { func (gv *promGaugeVec) Set(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Set(v) gv.gauge.WithLabelValues(labels...).Set(v)
} }

View File

@@ -21,6 +21,7 @@ func TestNewGaugeVec(t *testing.T) {
} }
func TestGaugeInc(t *testing.T) { func TestGaugeInc(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{ gaugeVec := NewGaugeVec(&GaugeVecOpts{
Namespace: "rpc_client2", Namespace: "rpc_client2",
Subsystem: "requests", Subsystem: "requests",
@@ -37,6 +38,7 @@ func TestGaugeInc(t *testing.T) {
} }
func TestGaugeAdd(t *testing.T) { func TestGaugeAdd(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{ gaugeVec := NewGaugeVec(&GaugeVecOpts{
Namespace: "rpc_client", Namespace: "rpc_client",
Subsystem: "request", Subsystem: "request",
@@ -53,6 +55,7 @@ func TestGaugeAdd(t *testing.T) {
} }
func TestGaugeSet(t *testing.T) { func TestGaugeSet(t *testing.T) {
startAgent()
gaugeVec := NewGaugeVec(&GaugeVecOpts{ gaugeVec := NewGaugeVec(&GaugeVecOpts{
Namespace: "http_client", Namespace: "http_client",
Subsystem: "request", Subsystem: "request",

View File

@@ -3,6 +3,7 @@ package metric
import ( import (
prom "github.com/prometheus/client_golang/prometheus" prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc" "github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
) )
type ( type (
@@ -53,6 +54,10 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
} }
func (hv *promHistogramVec) Observe(v int64, labels ...string) { func (hv *promHistogramVec) Observe(v int64, labels ...string) {
if !prometheus.Enabled() {
return
}
hv.histogram.WithLabelValues(labels...).Observe(float64(v)) hv.histogram.WithLabelValues(labels...).Observe(float64(v))
} }

View File

@@ -21,6 +21,7 @@ func TestNewHistogramVec(t *testing.T) {
} }
func TestHistogramObserve(t *testing.T) { func TestHistogramObserve(t *testing.T) {
startAgent()
histogramVec := NewHistogramVec(&HistogramVecOpts{ histogramVec := NewHistogramVec(&HistogramVecOpts{
Name: "counts", Name: "counts",
Help: "rpc server requests duration(ms).", Help: "rpc server requests duration(ms).",

View File

@@ -102,12 +102,12 @@ func ForEach(generate GenerateFunc, mapper ForEachFunc, opts ...Option) {
options := buildOptions(opts...) options := buildOptions(opts...)
panicChan := &onceChan{channel: make(chan interface{})} panicChan := &onceChan{channel: make(chan interface{})}
source := buildSource(generate, panicChan) source := buildSource(generate, panicChan)
collector := make(chan interface{}, options.workers) collector := make(chan interface{})
done := make(chan lang.PlaceholderType) done := make(chan lang.PlaceholderType)
go executeMappers(mapperContext{ go executeMappers(mapperContext{
ctx: options.ctx, ctx: options.ctx,
mapper: func(item interface{}, writer Writer) { mapper: func(item interface{}, _ Writer) {
mapper(item) mapper(item)
}, },
source: source, source: source,
@@ -212,6 +212,8 @@ func mapReduceWithPanicChan(source <-chan interface{}, panicChan *onceChan, mapp
cancel(context.DeadlineExceeded) cancel(context.DeadlineExceeded)
return nil, context.DeadlineExceeded return nil, context.DeadlineExceeded
case v := <-panicChan.channel: case v := <-panicChan.channel:
// drain output here, otherwise for loop panic in defer
drain(output)
panic(v) panic(v)
case v, ok := <-output: case v, ok := <-output:
if err := retErr.Load(); err != nil { if err := retErr.Load(); err != nil {
@@ -376,9 +378,7 @@ type onceChan struct {
} }
func (oc *onceChan) write(val interface{}) { func (oc *onceChan) write(val interface{}) {
if atomic.AddInt32(&oc.wrote, 1) > 1 { if atomic.CompareAndSwapInt32(&oc.wrote, 0, 1) {
return oc.channel <- val
} }
oc.channel <- val
} }

View File

@@ -3,7 +3,7 @@ package mr
import ( import (
"context" "context"
"errors" "errors"
"io/ioutil" "io"
"log" "log"
"runtime" "runtime"
"sync/atomic" "sync/atomic"
@@ -17,7 +17,7 @@ import (
var errDummy = errors.New("dummy") var errDummy = errors.New("dummy")
func init() { func init() {
log.SetOutput(ioutil.Discard) log.SetOutput(io.Discard)
} }
func TestFinish(t *testing.T) { func TestFinish(t *testing.T) {

View File

@@ -1,16 +1,23 @@
package proc package proc
import ( import (
"log"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
) )
func TestDumpGoroutines(t *testing.T) { func TestDumpGoroutines(t *testing.T) {
var buf strings.Builder var buf strings.Builder
log.SetOutput(&buf) w := logx.NewWriter(&buf)
o := logx.Reset()
logx.SetWriter(w)
defer func() {
logx.Reset()
logx.SetWriter(o)
}()
dumpGoroutines() dumpGoroutines()
assert.True(t, strings.Contains(buf.String(), ".dump")) assert.True(t, strings.Contains(buf.String(), ".dump"))
} }

View File

@@ -1,16 +1,24 @@
package proc package proc
import ( import (
"log"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
) )
func TestProfile(t *testing.T) { func TestProfile(t *testing.T) {
var buf strings.Builder var buf strings.Builder
log.SetOutput(&buf) w := logx.NewWriter(&buf)
o := logx.Reset()
logx.SetWriter(w)
defer func() {
logx.Reset()
logx.SetWriter(o)
}()
profiler := StartProfile() profiler := StartProfile()
// start again should not work // start again should not work
assert.NotNil(t, StartProfile()) assert.NotNil(t, StartProfile())

View File

@@ -5,6 +5,7 @@ import (
"github.com/zeromicro/go-zero/core/load" "github.com/zeromicro/go-zero/core/load"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus" "github.com/zeromicro/go-zero/core/prometheus"
"github.com/zeromicro/go-zero/core/stat" "github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/trace" "github.com/zeromicro/go-zero/core/trace"
@@ -56,6 +57,9 @@ func (sc ServiceConf) SetUp() error {
sc.Telemetry.Name = sc.Name sc.Telemetry.Name = sc.Name
} }
trace.StartAgent(sc.Telemetry) trace.StartAgent(sc.Telemetry)
proc.AddShutdownListener(func() {
trace.StopAgent()
})
if len(sc.MetricsUrl) > 0 { if len(sc.MetricsUrl) > 0 {
stat.SetReportWriter(stat.NewRemoteWriter(sc.MetricsUrl)) stat.SetReportWriter(stat.NewRemoteWriter(sc.MetricsUrl))

View File

@@ -15,7 +15,6 @@ import (
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/proc" "github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/sysx" "github.com/zeromicro/go-zero/core/sysx"
"github.com/zeromicro/go-zero/core/timex"
) )
const ( const (
@@ -47,7 +46,7 @@ func Report(msg string) {
if fn != nil { if fn != nil {
reported := lessExecutor.DoOrDiscard(func() { reported := lessExecutor.DoOrDiscard(func() {
var builder strings.Builder var builder strings.Builder
fmt.Fprintf(&builder, "%s\n", timex.Time().Format(timeFormat)) fmt.Fprintf(&builder, "%s\n", time.Now().Format(timeFormat))
if len(clusterName) > 0 { if len(clusterName) > 0 {
fmt.Fprintf(&builder, "cluster: %s\n", clusterName) fmt.Fprintf(&builder, "cluster: %s\n", clusterName)
} }

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"time" "time"
"github.com/zeromicro/go-zero/core/iox" "github.com/zeromicro/go-zero/core/iox"
@@ -20,10 +21,11 @@ var (
preTotal uint64 preTotal uint64
quota float64 quota float64
cores uint64 cores uint64
initOnce sync.Once
) )
// if /proc not present, ignore the cpu calculation, like wsl linux // if /proc not present, ignore the cpu calculation, like wsl linux
func init() { func initialize() {
cpus, err := cpuSets() cpus, err := cpuSets()
if err != nil { if err != nil {
logx.Error(err) logx.Error(err)
@@ -69,10 +71,13 @@ func init() {
// RefreshCpu refreshes cpu usage and returns. // RefreshCpu refreshes cpu usage and returns.
func RefreshCpu() uint64 { func RefreshCpu() uint64 {
initOnce.Do(initialize)
total, err := totalCpuUsage() total, err := totalCpuUsage()
if err != nil { if err != nil {
return 0 return 0
} }
system, err := systemCpuUsage() system, err := systemCpuUsage()
if err != nil { if err != nil {
return 0 return 0

View File

@@ -10,7 +10,10 @@ import (
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
const httpTimeout = time.Second * 5 const (
httpTimeout = time.Second * 5
jsonContentType = "application/json; charset=utf-8"
)
// ErrWriteFailed is an error that indicates failed to submit a StatReport. // ErrWriteFailed is an error that indicates failed to submit a StatReport.
var ErrWriteFailed = errors.New("submit failed") var ErrWriteFailed = errors.New("submit failed")
@@ -36,7 +39,7 @@ func (rw *RemoteWriter) Write(report *StatReport) error {
client := &http.Client{ client := &http.Client{
Timeout: httpTimeout, Timeout: httpTimeout,
} }
resp, err := client.Post(rw.endpoint, "application/json", bytes.NewBuffer(bs)) resp, err := client.Post(rw.endpoint, jsonContentType, bytes.NewReader(bs))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"math/rand" "math/rand"
"sync" "sync"
"time" "time"
@@ -123,13 +124,14 @@ func (c cacheNode) SetWithExpire(key string, val interface{}, expire time.Durati
} }
// SetWithExpireCtx sets the cache with key and v, using given expire. // SetWithExpireCtx sets the cache with key and v, using given expire.
func (c cacheNode) SetWithExpireCtx(ctx context.Context, key string, val interface{}, expire time.Duration) error { func (c cacheNode) SetWithExpireCtx(ctx context.Context, key string, val interface{},
expire time.Duration) error {
data, err := jsonx.Marshal(val) data, err := jsonx.Marshal(val)
if err != nil { if err != nil {
return err return err
} }
return c.rds.SetexCtx(ctx, key, string(data), int(expire.Seconds())) return c.rds.SetexCtx(ctx, key, string(data), int(math.Ceil(expire.Seconds())))
} }
// String returns a string that represents the cacheNode. // String returns a string that represents the cacheNode.
@@ -145,7 +147,8 @@ func (c cacheNode) Take(val interface{}, key string, query func(val interface{})
// TakeCtx takes the result from cache first, if not found, // TakeCtx takes the result from cache first, if not found,
// query from DB and set cache using c.expiry, then return the result. // query from DB and set cache using c.expiry, then return the result.
func (c cacheNode) TakeCtx(ctx context.Context, val interface{}, key string, query func(val interface{}) error) error { func (c cacheNode) TakeCtx(ctx context.Context, val interface{}, key string,
query func(val interface{}) error) error {
return c.doTake(ctx, val, key, query, func(v interface{}) error { return c.doTake(ctx, val, key, query, func(v interface{}) error {
return c.SetCtx(ctx, key, v) return c.SetCtx(ctx, key, v)
}) })
@@ -153,13 +156,15 @@ func (c cacheNode) TakeCtx(ctx context.Context, val interface{}, key string, que
// TakeWithExpire takes the result from cache first, if not found, // TakeWithExpire takes the result from cache first, if not found,
// query from DB and set cache using given expire, then return the result. // query from DB and set cache using given expire, then return the result.
func (c cacheNode) TakeWithExpire(val interface{}, key string, query func(val interface{}, expire time.Duration) error) error { func (c cacheNode) TakeWithExpire(val interface{}, key string, query func(val interface{},
expire time.Duration) error) error {
return c.TakeWithExpireCtx(context.Background(), val, key, query) return c.TakeWithExpireCtx(context.Background(), val, key, query)
} }
// TakeWithExpireCtx takes the result from cache first, if not found, // TakeWithExpireCtx takes the result from cache first, if not found,
// query from DB and set cache using given expire, then return the result. // query from DB and set cache using given expire, then return the result.
func (c cacheNode) TakeWithExpireCtx(ctx context.Context, val interface{}, key string, query func(val interface{}, expire time.Duration) error) error { func (c cacheNode) TakeWithExpireCtx(ctx context.Context, val interface{}, key string,
query func(val interface{}, expire time.Duration) error) error {
expire := c.aroundDuration(c.expiry) expire := c.aroundDuration(c.expiry)
return c.doTake(ctx, val, key, func(v interface{}) error { return c.doTake(ctx, val, key, func(v interface{}) error {
return query(v, expire) return query(v, expire)
@@ -239,7 +244,11 @@ func (c cacheNode) doTake(ctx context.Context, v interface{}, key string,
return nil return nil
} }
// got the result from previous ongoing query // got the result from previous ongoing query.
// why not call IncrementTotal at the beginning of this function?
// because a shared error is returned, and we don't want to count.
// for example, if the db is down, the query will be failed, we count
// the shared errors with one db failure.
c.stat.IncrementTotal() c.stat.IncrementTotal()
c.stat.IncrementHit() c.stat.IncrementHit()
@@ -267,5 +276,6 @@ func (c cacheNode) processCache(ctx context.Context, key, data string, v interfa
} }
func (c cacheNode) setCacheWithNotFound(ctx context.Context, key string) error { func (c cacheNode) setCacheWithNotFound(ctx context.Context, key string) error {
return c.rds.SetexCtx(ctx, key, notFoundPlaceholder, int(c.aroundDuration(c.notFoundExpiry).Seconds())) seconds := int(math.Ceil(c.aroundDuration(c.notFoundExpiry).Seconds()))
return c.rds.SetexCtx(ctx, key, notFoundPlaceholder, seconds)
} }

View File

@@ -88,7 +88,7 @@ func TestCacheNode_InvalidCache(t *testing.T) {
assert.Equal(t, miniredis.ErrKeyNotFound, err) assert.Equal(t, miniredis.ErrKeyNotFound, err)
} }
func TestCacheNode_Take(t *testing.T) { func TestCacheNode_SetWithExpire(t *testing.T) {
store, clean, err := redistest.CreateRedis() store, clean, err := redistest.CreateRedis()
assert.Nil(t, err) assert.Nil(t, err)
defer clean() defer clean()
@@ -100,8 +100,18 @@ func TestCacheNode_Take(t *testing.T) {
lock: new(sync.Mutex), lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation), unstableExpiry: mathx.NewUnstable(expiryDeviation),
stat: NewStat("any"), stat: NewStat("any"),
errNotFound: errTestNotFound, errNotFound: errors.New("any"),
} }
assert.NotNil(t, cn.SetWithExpire("key", make(chan int), time.Second))
}
func TestCacheNode_Take(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
cn := NewNode(store, syncx.NewSingleFlight(), NewStat("any"), errTestNotFound,
WithExpiry(time.Second), WithNotFoundExpiry(time.Second))
var str string var str string
err = cn.Take(&str, "any", func(v interface{}) error { err = cn.Take(&str, "any", func(v interface{}) error {
*v.(*string) = "value" *v.(*string) = "value"

View File

@@ -2,7 +2,7 @@ package clickhouse
import ( import (
// imports the driver, don't remove this comment, golint requires. // imports the driver, don't remove this comment, golint requires.
_ "github.com/ClickHouse/clickhouse-go" _ "github.com/ClickHouse/clickhouse-go/v2"
"github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stores/sqlx"
) )

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ func TestRedis_Decr(t *testing.T) {
_, err := store.Decr("a") _, err := store.Decr("a")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.Decr("a") val, err := client.Decr("a")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(-1), val) assert.Equal(t, int64(-1), val)
@@ -37,7 +37,7 @@ func TestRedis_DecrBy(t *testing.T) {
_, err := store.Incrby("a", 2) _, err := store.Incrby("a", 2)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.Decrby("a", 2) val, err := client.Decrby("a", 2)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(-2), val) assert.Equal(t, int64(-2), val)
@@ -52,7 +52,7 @@ func TestRedis_Exists(t *testing.T) {
_, err := store.Exists("foo") _, err := store.Exists("foo")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
ok, err := client.Exists("a") ok, err := client.Exists("a")
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, ok) assert.False(t, ok)
@@ -68,7 +68,7 @@ func TestRedis_Eval(t *testing.T) {
_, err := store.Eval(`redis.call("EXISTS", KEYS[1])`, "key1") _, err := store.Eval(`redis.call("EXISTS", KEYS[1])`, "key1")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
_, err := client.Eval(`redis.call("EXISTS", KEYS[1])`, "notexist") _, err := client.Eval(`redis.call("EXISTS", KEYS[1])`, "notexist")
assert.Equal(t, redis.Nil, err) assert.Equal(t, redis.Nil, err)
err = client.Set("key1", "value1") err = client.Set("key1", "value1")
@@ -88,7 +88,7 @@ func TestRedis_Hgetall(t *testing.T) {
_, err = store.Hgetall("a") _, err = store.Hgetall("a")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa")) assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb")) assert.Nil(t, client.Hset("a", "bb", "bbb"))
vals, err := client.Hgetall("a") vals, err := client.Hgetall("a")
@@ -105,7 +105,7 @@ func TestRedis_Hvals(t *testing.T) {
_, err := store.Hvals("a") _, err := store.Hvals("a")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa")) assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb")) assert.Nil(t, client.Hset("a", "bb", "bbb"))
vals, err := client.Hvals("a") vals, err := client.Hvals("a")
@@ -119,7 +119,7 @@ func TestRedis_Hsetnx(t *testing.T) {
_, err := store.Hsetnx("a", "dd", "ddd") _, err := store.Hsetnx("a", "dd", "ddd")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa")) assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb")) assert.Nil(t, client.Hset("a", "bb", "bbb"))
ok, err := client.Hsetnx("a", "bb", "ccc") ok, err := client.Hsetnx("a", "bb", "ccc")
@@ -141,7 +141,7 @@ func TestRedis_HdelHlen(t *testing.T) {
_, err = store.Hlen("a") _, err = store.Hlen("a")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa")) assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb")) assert.Nil(t, client.Hset("a", "bb", "bbb"))
num, err := client.Hlen("a") num, err := client.Hlen("a")
@@ -161,7 +161,7 @@ func TestRedis_HIncrBy(t *testing.T) {
_, err := store.Hincrby("key", "field", 3) _, err := store.Hincrby("key", "field", 3)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.Hincrby("key", "field", 2) val, err := client.Hincrby("key", "field", 2)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, val) assert.Equal(t, 2, val)
@@ -176,7 +176,7 @@ func TestRedis_Hkeys(t *testing.T) {
_, err := store.Hkeys("a") _, err := store.Hkeys("a")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa")) assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb")) assert.Nil(t, client.Hset("a", "bb", "bbb"))
vals, err := client.Hkeys("a") vals, err := client.Hkeys("a")
@@ -190,7 +190,7 @@ func TestRedis_Hmget(t *testing.T) {
_, err := store.Hmget("a", "aa", "bb") _, err := store.Hmget("a", "aa", "bb")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hset("a", "aa", "aaa")) assert.Nil(t, client.Hset("a", "aa", "aaa"))
assert.Nil(t, client.Hset("a", "bb", "bbb")) assert.Nil(t, client.Hset("a", "bb", "bbb"))
vals, err := client.Hmget("a", "aa", "bb") vals, err := client.Hmget("a", "aa", "bb")
@@ -209,7 +209,7 @@ func TestRedis_Hmset(t *testing.T) {
}) })
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
assert.Nil(t, client.Hmset("a", map[string]string{ assert.Nil(t, client.Hmset("a", map[string]string{
"aa": "aaa", "aa": "aaa",
"bb": "bbb", "bb": "bbb",
@@ -225,7 +225,7 @@ func TestRedis_Incr(t *testing.T) {
_, err := store.Incr("a") _, err := store.Incr("a")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.Incr("a") val, err := client.Incr("a")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(1), val) assert.Equal(t, int64(1), val)
@@ -240,7 +240,7 @@ func TestRedis_IncrBy(t *testing.T) {
_, err := store.Incrby("a", 2) _, err := store.Incrby("a", 2)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.Incrby("a", 2) val, err := client.Incrby("a", 2)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(2), val) assert.Equal(t, int64(2), val)
@@ -267,7 +267,7 @@ func TestRedis_List(t *testing.T) {
_, err = store.Lindex("key", 0) _, err = store.Lindex("key", 0)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.Lpush("key", "value1", "value2") val, err := client.Lpush("key", "value1", "value2")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, val) assert.Equal(t, 2, val)
@@ -316,7 +316,7 @@ func TestRedis_Persist(t *testing.T) {
err = store.Expireat("key", time.Now().Unix()+5) err = store.Expireat("key", time.Now().Unix()+5)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
ok, err := client.Persist("key") ok, err := client.Persist("key")
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, ok) assert.False(t, ok)
@@ -348,7 +348,7 @@ func TestRedis_Sscan(t *testing.T) {
_, err = store.Del(key) _, err = store.Del(key)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
var list []string var list []string
for i := 0; i < 1550; i++ { for i := 0; i < 1550; i++ {
list = append(list, stringx.Randn(i)) list = append(list, stringx.Randn(i))
@@ -390,7 +390,7 @@ func TestRedis_Set(t *testing.T) {
_, err = store.Spop("key") _, err = store.Spop("key")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
num, err := client.Sadd("key", 1, 2, 3, 4) num, err := client.Sadd("key", 1, 2, 3, 4)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 4, num) assert.Equal(t, 4, num)
@@ -434,7 +434,7 @@ func TestRedis_SetGetDel(t *testing.T) {
_, err = store.Del("hello") _, err = store.Del("hello")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
err := client.Set("hello", "world") err := client.Set("hello", "world")
assert.Nil(t, err) assert.Nil(t, err)
val, err := client.Get("hello") val, err := client.Get("hello")
@@ -457,7 +457,7 @@ func TestRedis_SetExNx(t *testing.T) {
_, err = store.SetnxEx("newhello", "newworld", 5) _, err = store.SetnxEx("newhello", "newworld", 5)
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
err := client.Setex("hello", "world", 5) err := client.Setex("hello", "world", 5)
assert.Nil(t, err) assert.Nil(t, err)
ok, err := client.Setnx("hello", "newworld") ok, err := client.Setnx("hello", "newworld")
@@ -495,7 +495,7 @@ func TestRedis_Getset(t *testing.T) {
_, err := store.GetSet("hello", "world") _, err := store.GetSet("hello", "world")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
val, err := client.GetSet("hello", "world") val, err := client.GetSet("hello", "world")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "", val) assert.Equal(t, "", val)
@@ -524,7 +524,7 @@ func TestRedis_SetGetDelHashField(t *testing.T) {
_, err = store.Hdel("key", "field") _, err = store.Hdel("key", "field")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
err := client.Hset("key", "field", "value") err := client.Hset("key", "field", "value")
assert.Nil(t, err) assert.Nil(t, err)
val, err := client.Hget("key", "field") val, err := client.Hget("key", "field")
@@ -587,8 +587,8 @@ func TestRedis_SortedSet(t *testing.T) {
}) })
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(client Store) { runOnCluster(func(client Store) {
ok, err := client.Zadd("key", 1, "value1") ok, err := client.ZaddFloat("key", 1, "value1")
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, ok) assert.True(t, ok)
ok, err = client.Zadd("key", 2, "value1") ok, err = client.Zadd("key", 2, "value1")
@@ -724,7 +724,7 @@ func TestRedis_HyperLogLog(t *testing.T) {
_, err = store.Pfcount("key") _, err = store.Pfcount("key")
assert.NotNil(t, err) assert.NotNil(t, err)
runOnCluster(t, func(cluster Store) { runOnCluster(func(cluster Store) {
ok, err := cluster.Pfadd("key", "value") ok, err := cluster.Pfadd("key", "value")
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, ok) assert.True(t, ok)
@@ -734,7 +734,7 @@ func TestRedis_HyperLogLog(t *testing.T) {
}) })
} }
func runOnCluster(t *testing.T, fn func(cluster Store)) { func runOnCluster(fn func(cluster Store)) {
s1.FlushAll() s1.FlushAll()
s2.FlushAll() s2.FlushAll()

View File

@@ -0,0 +1,91 @@
package mon
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/executors"
"github.com/zeromicro/go-zero/core/logx"
"go.mongodb.org/mongo-driver/mongo"
)
const (
flushInterval = time.Second
maxBulkRows = 1000
)
type (
// ResultHandler is a handler that used to handle results.
ResultHandler func(*mongo.InsertManyResult, error)
// A BulkInserter is used to insert bulk of mongo records.
BulkInserter struct {
executor *executors.PeriodicalExecutor
inserter *dbInserter
}
)
// NewBulkInserter returns a BulkInserter.
func NewBulkInserter(coll *mongo.Collection, interval ...time.Duration) *BulkInserter {
inserter := &dbInserter{
collection: coll,
}
duration := flushInterval
if len(interval) > 0 {
duration = interval[0]
}
return &BulkInserter{
executor: executors.NewPeriodicalExecutor(duration, inserter),
inserter: inserter,
}
}
// Flush flushes the inserter, writes all pending records.
func (bi *BulkInserter) Flush() {
bi.executor.Flush()
}
// Insert inserts doc.
func (bi *BulkInserter) Insert(doc interface{}) {
bi.executor.Add(doc)
}
// SetResultHandler sets the result handler.
func (bi *BulkInserter) SetResultHandler(handler ResultHandler) {
bi.executor.Sync(func() {
bi.inserter.resultHandler = handler
})
}
type dbInserter struct {
collection *mongo.Collection
documents []interface{}
resultHandler ResultHandler
}
func (in *dbInserter) AddTask(doc interface{}) bool {
in.documents = append(in.documents, doc)
return len(in.documents) >= maxBulkRows
}
func (in *dbInserter) Execute(objs interface{}) {
docs := objs.([]interface{})
if len(docs) == 0 {
return
}
result, err := in.collection.InsertMany(context.Background(), docs)
if in.resultHandler != nil {
in.resultHandler(result, err)
} else if err != nil {
logx.Error(err)
}
}
func (in *dbInserter) RemoveAll() interface{} {
documents := in.documents
in.documents = nil
return documents
}

View File

@@ -0,0 +1,27 @@
package mon
import (
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)
func TestBulkInserter(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "ok", Value: 1}}...))
bulk := NewBulkInserter(mt.Coll)
bulk.SetResultHandler(func(result *mongo.InsertManyResult, err error) {
assert.Nil(t, err)
assert.Equal(t, 2, len(result.InsertedIDs))
})
bulk.Insert(bson.D{{Key: "foo", Value: "bar"}})
bulk.Insert(bson.D{{Key: "foo", Value: "baz"}})
bulk.Flush()
})
}

View File

@@ -0,0 +1,51 @@
package mon
import (
"context"
"io"
"time"
"github.com/zeromicro/go-zero/core/syncx"
"go.mongodb.org/mongo-driver/mongo"
mopt "go.mongodb.org/mongo-driver/mongo/options"
)
const defaultTimeout = time.Second
var clientManager = syncx.NewResourceManager()
// ClosableClient wraps *mongo.Client and provides a Close method.
type ClosableClient struct {
*mongo.Client
}
// Close disconnects the underlying *mongo.Client.
func (cs *ClosableClient) Close() error {
return cs.Client.Disconnect(context.Background())
}
// Inject injects a *mongo.Client into the client manager.
// Typically, this is used to inject a *mongo.Client for test purpose.
func Inject(key string, client *mongo.Client) {
clientManager.Inject(key, &ClosableClient{client})
}
func getClient(url string) (*mongo.Client, error) {
val, err := clientManager.GetResource(url, func() (io.Closer, error) {
cli, err := mongo.Connect(context.Background(), mopt.Client().ApplyURI(url))
if err != nil {
return nil, err
}
concurrentSess := &ClosableClient{
Client: cli,
}
return concurrentSess, nil
})
if err != nil {
return nil, err
}
return val.(*ClosableClient).Client, nil
}

View File

@@ -0,0 +1,20 @@
package mon
import (
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)
func TestClientManger_getClient(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
Inject(mtest.ClusterURI(), mt.Client)
cli, err := getClient(mtest.ClusterURI())
assert.Nil(t, err)
assert.Equal(t, mt.Client, cli)
})
}

View File

@@ -0,0 +1,572 @@
package mon
import (
"context"
"encoding/json"
"time"
"github.com/zeromicro/go-zero/core/breaker"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/timex"
"go.mongodb.org/mongo-driver/mongo"
mopt "go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
)
const (
defaultSlowThreshold = time.Millisecond * 500
// spanName is the span name of the mongo calls.
spanName = "mongo"
// mongodb method names
aggregate = "Aggregate"
bulkWrite = "BulkWrite"
countDocuments = "CountDocuments"
deleteMany = "DeleteMany"
deleteOne = "DeleteOne"
distinct = "Distinct"
estimatedDocumentCount = "EstimatedDocumentCount"
find = "Find"
findOne = "FindOne"
findOneAndDelete = "FindOneAndDelete"
findOneAndReplace = "FindOneAndReplace"
findOneAndUpdate = "FindOneAndUpdate"
insertMany = "InsertMany"
insertOne = "InsertOne"
replaceOne = "ReplaceOne"
updateByID = "UpdateByID"
updateMany = "UpdateMany"
updateOne = "UpdateOne"
)
// ErrNotFound is an alias of mongo.ErrNoDocuments
var ErrNotFound = mongo.ErrNoDocuments
type (
// Collection defines a MongoDB collection.
Collection interface {
// Aggregate executes an aggregation pipeline.
Aggregate(ctx context.Context, pipeline interface{}, opts ...*mopt.AggregateOptions) (
*mongo.Cursor, error)
// BulkWrite performs a bulk write operation.
BulkWrite(ctx context.Context, models []mongo.WriteModel, opts ...*mopt.BulkWriteOptions) (
*mongo.BulkWriteResult, error)
// Clone creates a copy of this collection with the same settings.
Clone(opts ...*mopt.CollectionOptions) (*mongo.Collection, error)
// CountDocuments returns the number of documents in the collection that match the filter.
CountDocuments(ctx context.Context, filter interface{}, opts ...*mopt.CountOptions) (int64, error)
// Database returns the database that this collection is a part of.
Database() *mongo.Database
// DeleteMany deletes documents from the collection that match the filter.
DeleteMany(ctx context.Context, filter interface{}, opts ...*mopt.DeleteOptions) (
*mongo.DeleteResult, error)
// DeleteOne deletes at most one document from the collection that matches the filter.
DeleteOne(ctx context.Context, filter interface{}, opts ...*mopt.DeleteOptions) (
*mongo.DeleteResult, error)
// Distinct returns a list of distinct values for the given key across the collection.
Distinct(ctx context.Context, fieldName string, filter interface{},
opts ...*mopt.DistinctOptions) ([]interface{}, error)
// Drop drops this collection from database.
Drop(ctx context.Context) error
// EstimatedDocumentCount returns an estimate of the count of documents in a collection
// using collection metadata.
EstimatedDocumentCount(ctx context.Context, opts ...*mopt.EstimatedDocumentCountOptions) (int64, error)
// Find finds the documents matching the provided filter.
Find(ctx context.Context, filter interface{}, opts ...*mopt.FindOptions) (*mongo.Cursor, error)
// FindOne returns up to one document that matches the provided filter.
FindOne(ctx context.Context, filter interface{}, opts ...*mopt.FindOneOptions) (
*mongo.SingleResult, error)
// FindOneAndDelete returns at most one document that matches the filter. If the filter
// matches multiple documents, only the first document is deleted.
FindOneAndDelete(ctx context.Context, filter interface{}, opts ...*mopt.FindOneAndDeleteOptions) (
*mongo.SingleResult, error)
// FindOneAndReplace returns at most one document that matches the filter. If the filter
// matches multiple documents, FindOneAndReplace returns the first document in the
// collection that matches the filter.
FindOneAndReplace(ctx context.Context, filter interface{}, replacement interface{},
opts ...*mopt.FindOneAndReplaceOptions) (*mongo.SingleResult, error)
// FindOneAndUpdate returns at most one document that matches the filter. If the filter
// matches multiple documents, FindOneAndUpdate returns the first document in the
// collection that matches the filter.
FindOneAndUpdate(ctx context.Context, filter interface{}, update interface{},
opts ...*mopt.FindOneAndUpdateOptions) (*mongo.SingleResult, error)
// Indexes returns the index view for this collection.
Indexes() mongo.IndexView
// InsertMany inserts the provided documents.
InsertMany(ctx context.Context, documents []interface{}, opts ...*mopt.InsertManyOptions) (
*mongo.InsertManyResult, error)
// InsertOne inserts the provided document.
InsertOne(ctx context.Context, document interface{}, opts ...*mopt.InsertOneOptions) (
*mongo.InsertOneResult, error)
// ReplaceOne replaces at most one document that matches the filter.
ReplaceOne(ctx context.Context, filter interface{}, replacement interface{},
opts ...*mopt.ReplaceOptions) (*mongo.UpdateResult, error)
// UpdateByID updates a single document matching the provided filter.
UpdateByID(ctx context.Context, id interface{}, update interface{},
opts ...*mopt.UpdateOptions) (*mongo.UpdateResult, error)
// UpdateMany updates the provided documents.
UpdateMany(ctx context.Context, filter interface{}, update interface{},
opts ...*mopt.UpdateOptions) (*mongo.UpdateResult, error)
// UpdateOne updates a single document matching the provided filter.
UpdateOne(ctx context.Context, filter interface{}, update interface{},
opts ...*mopt.UpdateOptions) (*mongo.UpdateResult, error)
// Watch returns a change stream cursor used to receive notifications of changes to the collection.
Watch(ctx context.Context, pipeline interface{}, opts ...*mopt.ChangeStreamOptions) (
*mongo.ChangeStream, error)
}
decoratedCollection struct {
*mongo.Collection
name string
brk breaker.Breaker
}
keepablePromise struct {
promise breaker.Promise
log func(error)
}
)
func newCollection(collection *mongo.Collection, brk breaker.Breaker) Collection {
return &decoratedCollection{
Collection: collection,
name: collection.Name(),
brk: brk,
}
}
func (c *decoratedCollection) Aggregate(ctx context.Context, pipeline interface{},
opts ...*mopt.AggregateOptions) (cur *mongo.Cursor, err error) {
ctx, span := startSpan(ctx, aggregate)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, aggregate, starTime, err)
}()
cur, err = c.Collection.Aggregate(ctx, pipeline, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) BulkWrite(ctx context.Context, models []mongo.WriteModel,
opts ...*mopt.BulkWriteOptions) (res *mongo.BulkWriteResult, err error) {
ctx, span := startSpan(ctx, bulkWrite)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, bulkWrite, startTime, err)
}()
res, err = c.Collection.BulkWrite(ctx, models, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) CountDocuments(ctx context.Context, filter interface{},
opts ...*mopt.CountOptions) (count int64, err error) {
ctx, span := startSpan(ctx, countDocuments)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, countDocuments, startTime, err)
}()
count, err = c.Collection.CountDocuments(ctx, filter, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) DeleteMany(ctx context.Context, filter interface{},
opts ...*mopt.DeleteOptions) (res *mongo.DeleteResult, err error) {
ctx, span := startSpan(ctx, deleteMany)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, deleteMany, startTime, err)
}()
res, err = c.Collection.DeleteMany(ctx, filter, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) DeleteOne(ctx context.Context, filter interface{},
opts ...*mopt.DeleteOptions) (res *mongo.DeleteResult, err error) {
ctx, span := startSpan(ctx, deleteOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, deleteOne, startTime, err, filter)
}()
res, err = c.Collection.DeleteOne(ctx, filter, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) Distinct(ctx context.Context, fieldName string, filter interface{},
opts ...*mopt.DistinctOptions) (val []interface{}, err error) {
ctx, span := startSpan(ctx, distinct)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, distinct, startTime, err)
}()
val, err = c.Collection.Distinct(ctx, fieldName, filter, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) EstimatedDocumentCount(ctx context.Context,
opts ...*mopt.EstimatedDocumentCountOptions) (val int64, err error) {
ctx, span := startSpan(ctx, estimatedDocumentCount)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, estimatedDocumentCount, startTime, err)
}()
val, err = c.Collection.EstimatedDocumentCount(ctx, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) Find(ctx context.Context, filter interface{},
opts ...*mopt.FindOptions) (cur *mongo.Cursor, err error) {
ctx, span := startSpan(ctx, find)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, find, startTime, err, filter)
}()
cur, err = c.Collection.Find(ctx, filter, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) FindOne(ctx context.Context, filter interface{},
opts ...*mopt.FindOneOptions) (res *mongo.SingleResult, err error) {
ctx, span := startSpan(ctx, findOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, findOne, startTime, err, filter)
}()
res = c.Collection.FindOne(ctx, filter, opts...)
err = res.Err()
return err
}, acceptable)
return
}
func (c *decoratedCollection) FindOneAndDelete(ctx context.Context, filter interface{},
opts ...*mopt.FindOneAndDeleteOptions) (res *mongo.SingleResult, err error) {
ctx, span := startSpan(ctx, findOneAndDelete)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, findOneAndDelete, startTime, err, filter)
}()
res = c.Collection.FindOneAndDelete(ctx, filter, opts...)
err = res.Err()
return err
}, acceptable)
return
}
func (c *decoratedCollection) FindOneAndReplace(ctx context.Context, filter interface{},
replacement interface{}, opts ...*mopt.FindOneAndReplaceOptions) (
res *mongo.SingleResult, err error) {
ctx, span := startSpan(ctx, findOneAndReplace)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, findOneAndReplace, startTime, err, filter, replacement)
}()
res = c.Collection.FindOneAndReplace(ctx, filter, replacement, opts...)
err = res.Err()
return err
}, acceptable)
return
}
func (c *decoratedCollection) FindOneAndUpdate(ctx context.Context, filter interface{}, update interface{},
opts ...*mopt.FindOneAndUpdateOptions) (res *mongo.SingleResult, err error) {
ctx, span := startSpan(ctx, findOneAndUpdate)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, findOneAndUpdate, startTime, err, filter, update)
}()
res = c.Collection.FindOneAndUpdate(ctx, filter, update, opts...)
err = res.Err()
return err
}, acceptable)
return
}
func (c *decoratedCollection) InsertMany(ctx context.Context, documents []interface{},
opts ...*mopt.InsertManyOptions) (res *mongo.InsertManyResult, err error) {
ctx, span := startSpan(ctx, insertMany)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, insertMany, startTime, err)
}()
res, err = c.Collection.InsertMany(ctx, documents, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) InsertOne(ctx context.Context, document interface{},
opts ...*mopt.InsertOneOptions) (res *mongo.InsertOneResult, err error) {
ctx, span := startSpan(ctx, insertOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, insertOne, startTime, err, document)
}()
res, err = c.Collection.InsertOne(ctx, document, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) ReplaceOne(ctx context.Context, filter interface{}, replacement interface{},
opts ...*mopt.ReplaceOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx, replaceOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, replaceOne, startTime, err, filter, replacement)
}()
res, err = c.Collection.ReplaceOne(ctx, filter, replacement, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) UpdateByID(ctx context.Context, id interface{}, update interface{},
opts ...*mopt.UpdateOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx, updateByID)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, updateByID, startTime, err, id, update)
}()
res, err = c.Collection.UpdateByID(ctx, id, update, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) UpdateMany(ctx context.Context, filter interface{}, update interface{},
opts ...*mopt.UpdateOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx, updateMany)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple(ctx, updateMany, startTime, err)
}()
res, err = c.Collection.UpdateMany(ctx, filter, update, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) UpdateOne(ctx context.Context, filter interface{}, update interface{},
opts ...*mopt.UpdateOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx, updateOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration(ctx, updateOne, startTime, err, filter, update)
}()
res, err = c.Collection.UpdateOne(ctx, filter, update, opts...)
return err
}, acceptable)
return
}
func (c *decoratedCollection) logDuration(ctx context.Context, method string,
startTime time.Duration, err error, docs ...interface{}) {
duration := timex.Since(startTime)
logger := logx.WithContext(ctx).WithDuration(duration)
content, jerr := json.Marshal(docs)
// jerr should not be non-nil, but we don't care much on this,
// if non-nil, we just log without docs.
if jerr != nil {
if err != nil {
if duration > slowThreshold.Load() {
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s)", c.name, method, err.Error())
} else {
logger.Infof("mongo(%s) - %s - fail(%s)", c.name, method, err.Error())
}
} else {
if duration > slowThreshold.Load() {
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok", c.name, method)
} else {
logger.Infof("mongo(%s) - %s - ok", c.name, method)
}
}
} else if err != nil {
if duration > slowThreshold.Load() {
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s) - %s",
c.name, method, err.Error(), string(content))
} else {
logger.Infof("mongo(%s) - %s - fail(%s) - %s",
c.name, method, err.Error(), string(content))
}
} else {
if duration > slowThreshold.Load() {
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s",
c.name, method, string(content))
} else {
logger.Infof("mongo(%s) - %s - ok - %s", c.name, method, string(content))
}
}
}
func (c *decoratedCollection) logDurationSimple(ctx context.Context, method string, startTime time.Duration, err error) {
logDuration(ctx, c.name, method, startTime, err)
}
func (p keepablePromise) accept(err error) error {
p.promise.Accept()
p.log(err)
return err
}
func (p keepablePromise) keep(err error) error {
if acceptable(err) {
p.promise.Accept()
} else {
p.promise.Reject(err.Error())
}
p.log(err)
return err
}
func acceptable(err error) bool {
return err == nil || err == mongo.ErrNoDocuments || err == mongo.ErrNilValue ||
err == mongo.ErrNilDocument || err == mongo.ErrNilCursor || err == mongo.ErrEmptySlice ||
// session errors
err == session.ErrSessionEnded || err == session.ErrNoTransactStarted ||
err == session.ErrTransactInProgress || err == session.ErrAbortAfterCommit ||
err == session.ErrAbortTwice || err == session.ErrCommitAfterAbort ||
err == session.ErrUnackWCUnsupported || err == session.ErrSnapshotTransaction
}

View File

@@ -0,0 +1,663 @@
package mon
import (
"context"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/breaker"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/core/timex"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
mopt "go.mongodb.org/mongo-driver/mongo/options"
)
var errDummy = errors.New("dummy")
func TestKeepPromise_accept(t *testing.T) {
p := new(mockPromise)
kp := keepablePromise{
promise: p,
log: func(error) {},
}
assert.Nil(t, kp.accept(nil))
assert.Equal(t, ErrNotFound, kp.accept(ErrNotFound))
}
func TestKeepPromise_keep(t *testing.T) {
tests := []struct {
err error
accepted bool
reason string
}{
{
err: nil,
accepted: true,
reason: "",
},
{
err: ErrNotFound,
accepted: true,
reason: "",
},
{
err: errors.New("any"),
accepted: false,
reason: "any",
},
}
for _, test := range tests {
t.Run(stringx.RandId(), func(t *testing.T) {
p := new(mockPromise)
kp := keepablePromise{
promise: p,
log: func(error) {},
}
assert.Equal(t, test.err, kp.keep(test.err))
assert.Equal(t, test.accepted, p.accepted)
assert.Equal(t, test.reason, p.reason)
})
}
}
func TestNewCollection(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
coll := mt.Coll
assert.NotNil(t, coll)
col := newCollection(coll, breaker.GetBreaker("localhost"))
assert.Equal(t, t.Name()+"/test", col.(*decoratedCollection).name)
})
}
func TestCollection_Aggregate(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
coll := mt.Coll
assert.NotNil(t, coll)
col := newCollection(coll, breaker.GetBreaker("localhost"))
ns := mt.Coll.Database().Name() + "." + mt.Coll.Name()
aggRes := mtest.CreateCursorResponse(1, ns, mtest.FirstBatch)
mt.AddMockResponses(aggRes)
assert.Equal(t, t.Name()+"/test", col.(*decoratedCollection).name)
cursor, err := col.Aggregate(context.Background(), mongo.Pipeline{}, mopt.Aggregate())
assert.Nil(t, err)
cursor.Close(context.Background())
})
}
func TestCollection_BulkWrite(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "ok", Value: 1}}...))
res, err := c.BulkWrite(context.Background(), []mongo.WriteModel{
mongo.NewInsertOneModel().SetDocument(bson.D{{Key: "foo", Value: 1}})},
)
assert.Nil(t, err)
assert.NotNil(t, res)
c.brk = new(dropBreaker)
_, err = c.BulkWrite(context.Background(), []mongo.WriteModel{
mongo.NewInsertOneModel().SetDocument(bson.D{{Key: "foo", Value: 1}})},
)
assert.Equal(t, errDummy, err)
})
}
func TestCollection_CountDocuments(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.FirstBatch,
bson.D{
{Key: "n", Value: 1},
}))
res, err := c.CountDocuments(context.Background(), bson.D{})
assert.Nil(t, err)
assert.Equal(t, int64(1), res)
c.brk = new(dropBreaker)
_, err = c.CountDocuments(context.Background(), bson.D{{Key: "foo", Value: 1}})
assert.Equal(t, errDummy, err)
})
}
func TestDecoratedCollection_DeleteMany(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
res, err := c.DeleteMany(context.Background(), bson.D{})
assert.Nil(t, err)
assert.Equal(t, int64(1), res.DeletedCount)
c.brk = new(dropBreaker)
_, err = c.DeleteMany(context.Background(), bson.D{{Key: "foo", Value: 1}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_Distinct(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(bson.D{{Key: "ok", Value: 1}, {Key: "values", Value: []int{1}}})
resp, err := c.Distinct(context.Background(), "foo", bson.D{})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp))
c.brk = new(dropBreaker)
_, err = c.Distinct(context.Background(), "foo", bson.D{{Key: "foo", Value: 1}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_EstimatedDocumentCount(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(bson.D{{Key: "ok", Value: 1}, {Key: "n", Value: 1}})
res, err := c.EstimatedDocumentCount(context.Background())
assert.Nil(t, err)
assert.Equal(t, int64(1), res)
c.brk = new(dropBreaker)
_, err = c.EstimatedDocumentCount(context.Background())
assert.Equal(t, errDummy, err)
})
}
func TestCollectionFind(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
find := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.FirstBatch,
bson.D{
{Key: "name", Value: "John"},
})
getMore := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.NextBatch,
bson.D{
{Key: "name", Value: "Mary"},
})
killCursors := mtest.CreateCursorResponse(
0,
"DBName.CollectionName",
mtest.NextBatch)
mt.AddMockResponses(find, getMore, killCursors)
filter := bson.D{{Key: "x", Value: 1}}
cursor, err := c.Find(context.Background(), filter, mopt.Find())
assert.Nil(t, err)
defer cursor.Close(context.Background())
var val []struct {
ID primitive.ObjectID `bson:"_id"`
Name string `bson:"name"`
}
assert.Nil(t, cursor.All(context.Background(), &val))
assert.Equal(t, 2, len(val))
assert.Equal(t, "John", val[0].Name)
assert.Equal(t, "Mary", val[1].Name)
c.brk = new(dropBreaker)
_, err = c.Find(context.Background(), filter, mopt.Find())
assert.Equal(t, errDummy, err)
})
}
func TestCollectionFindOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
find := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.FirstBatch,
bson.D{
{Key: "name", Value: "John"},
})
getMore := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.NextBatch,
bson.D{
{Key: "name", Value: "Mary"},
})
killCursors := mtest.CreateCursorResponse(
0,
"DBName.CollectionName",
mtest.NextBatch)
mt.AddMockResponses(find, getMore, killCursors)
filter := bson.D{{Key: "x", Value: 1}}
resp, err := c.FindOne(context.Background(), filter)
assert.Nil(t, err)
var val struct {
ID primitive.ObjectID `bson:"_id"`
Name string `bson:"name"`
}
assert.Nil(t, resp.Decode(&val))
assert.Equal(t, "John", val.Name)
c.brk = new(dropBreaker)
_, err = c.FindOne(context.Background(), filter)
assert.Equal(t, errDummy, err)
})
}
func TestCollection_FindOneAndDelete(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
filter := bson.D{}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{}...))
_, err := c.FindOneAndDelete(context.Background(), filter, mopt.FindOneAndDelete())
assert.Equal(t, mongo.ErrNoDocuments, err)
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
}...))
resp, err := c.FindOneAndDelete(context.Background(), filter, mopt.FindOneAndDelete())
assert.Nil(t, err)
var val struct {
Name string `bson:"name"`
}
assert.Nil(t, resp.Decode(&val))
assert.Equal(t, "John", val.Name)
c.brk = new(dropBreaker)
_, err = c.FindOneAndDelete(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_FindOneAndReplace(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{}...))
filter := bson.D{{Key: "x", Value: 1}}
replacement := bson.D{{Key: "x", Value: 2}}
opts := mopt.FindOneAndReplace().SetUpsert(true)
_, err := c.FindOneAndReplace(context.Background(), filter, replacement, opts)
assert.Equal(t, mongo.ErrNoDocuments, err)
mt.AddMockResponses(bson.D{{Key: "ok", Value: 1}, {Key: "value", Value: bson.D{
{Key: "name", Value: "John"},
}}})
resp, err := c.FindOneAndReplace(context.Background(), filter, replacement, opts)
assert.Nil(t, err)
var val struct {
Name string `bson:"name"`
}
assert.Nil(t, resp.Decode(&val))
assert.Equal(t, "John", val.Name)
c.brk = new(dropBreaker)
_, err = c.FindOneAndReplace(context.Background(), filter, replacement, opts)
assert.Equal(t, errDummy, err)
})
}
func TestCollection_FindOneAndUpdate(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(bson.D{{Key: "ok", Value: 1}})
filter := bson.D{{Key: "x", Value: 1}}
update := bson.D{{Key: "$x", Value: 2}}
opts := mopt.FindOneAndUpdate().SetUpsert(true)
_, err := c.FindOneAndUpdate(context.Background(), filter, update, opts)
assert.Equal(t, mongo.ErrNoDocuments, err)
mt.AddMockResponses(bson.D{{Key: "ok", Value: 1}, {Key: "value", Value: bson.D{
{Key: "name", Value: "John"},
}}})
resp, err := c.FindOneAndUpdate(context.Background(), filter, update, opts)
assert.Nil(t, err)
var val struct {
Name string `bson:"name"`
}
assert.Nil(t, resp.Decode(&val))
assert.Equal(t, "John", val.Name)
c.brk = new(dropBreaker)
_, err = c.FindOneAndUpdate(context.Background(), filter, update, opts)
assert.Equal(t, errDummy, err)
})
}
func TestCollection_InsertOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "ok", Value: 1}}...))
res, err := c.InsertOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Nil(t, err)
assert.NotNil(t, res)
c.brk = new(dropBreaker)
_, err = c.InsertOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_InsertMany(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "ok", Value: 1}}...))
res, err := c.InsertMany(context.Background(), []interface{}{
bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "foo", Value: "baz"}},
})
assert.Nil(t, err)
assert.NotNil(t, res)
assert.Equal(t, 2, len(res.InsertedIDs))
c.brk = new(dropBreaker)
_, err = c.InsertMany(context.Background(), []interface{}{bson.D{{Key: "foo", Value: "bar"}}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_Remove(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
res, err := c.DeleteOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Nil(t, err)
assert.Equal(t, int64(1), res.DeletedCount)
c.brk = new(dropBreaker)
_, err = c.DeleteOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Equal(t, errDummy, err)
})
}
func TestCollectionRemoveAll(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
res, err := c.DeleteMany(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Nil(t, err)
assert.Equal(t, int64(1), res.DeletedCount)
c.brk = new(dropBreaker)
_, err = c.DeleteMany(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_ReplaceOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
res, err := c.ReplaceOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "foo", Value: "baz"}},
)
assert.Nil(t, err)
assert.Equal(t, int64(1), res.MatchedCount)
c.brk = new(dropBreaker)
_, err = c.ReplaceOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "foo", Value: "baz"}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_UpdateOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
resp, err := c.UpdateOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "$set", Value: bson.D{{Key: "baz", Value: "qux"}}}})
assert.Nil(t, err)
assert.Equal(t, int64(1), resp.MatchedCount)
c.brk = new(dropBreaker)
_, err = c.UpdateOne(context.Background(), bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "$set", Value: bson.D{{Key: "baz", Value: "qux"}}}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_UpdateByID(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
resp, err := c.UpdateByID(context.Background(), primitive.NewObjectID(),
bson.D{{Key: "$set", Value: bson.D{{Key: "baz", Value: "qux"}}}})
assert.Nil(t, err)
assert.Equal(t, int64(1), resp.MatchedCount)
c.brk = new(dropBreaker)
_, err = c.UpdateByID(context.Background(), primitive.NewObjectID(),
bson.D{{Key: "$set", Value: bson.D{{Key: "baz", Value: "qux"}}}})
assert.Equal(t, errDummy, err)
})
}
func TestCollection_UpdateMany(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
resp, err := c.UpdateMany(context.Background(), bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "$set", Value: bson.D{{Key: "baz", Value: "qux"}}}})
assert.Nil(t, err)
assert.Equal(t, int64(1), resp.MatchedCount)
c.brk = new(dropBreaker)
_, err = c.UpdateMany(context.Background(), bson.D{{Key: "foo", Value: "bar"}},
bson.D{{Key: "$set", Value: bson.D{{Key: "baz", Value: "qux"}}}})
assert.Equal(t, errDummy, err)
})
}
func Test_DecoratedCollectionLogDuration(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
c := decoratedCollection{
Collection: mt.Coll,
brk: breaker.NewBreaker(),
}
var buf strings.Builder
w := logx.NewWriter(&buf)
o := logx.Reset()
logx.SetWriter(w)
defer func() {
logx.Reset()
logx.SetWriter(o)
}()
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now(), nil, "bar")
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "bar")
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now(), errors.New("bar"), make(chan int))
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "bar")
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now(), nil, make(chan int))
assert.Contains(t, buf.String(), "foo")
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now()-slowThreshold.Load()*2,
nil, make(chan int))
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "slowcall")
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now()-slowThreshold.Load()*2,
errors.New("bar"), make(chan int))
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "bar")
assert.Contains(t, buf.String(), "slowcall")
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now()-slowThreshold.Load()*2,
errors.New("bar"))
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "slowcall")
buf.Reset()
c.logDuration(context.Background(), "foo", timex.Now()-slowThreshold.Load()*2, nil)
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "slowcall")
}
type mockPromise struct {
accepted bool
reason string
}
func (p *mockPromise) Accept() {
p.accepted = true
}
func (p *mockPromise) Reject(reason string) {
p.reason = reason
}
type dropBreaker struct{}
func (d *dropBreaker) Name() string {
return "dummy"
}
func (d *dropBreaker) Allow() (breaker.Promise, error) {
return nil, errDummy
}
func (d *dropBreaker) Do(_ func() error) error {
return nil
}
func (d *dropBreaker) DoWithAcceptable(_ func() error, _ breaker.Acceptable) error {
return errDummy
}
func (d *dropBreaker) DoWithFallback(_ func() error, _ func(err error) error) error {
return nil
}
func (d *dropBreaker) DoWithFallbackAcceptable(_ func() error, _ func(err error) error,
_ breaker.Acceptable) error {
return nil
}

258
core/stores/mon/model.go Normal file
View File

@@ -0,0 +1,258 @@
package mon
import (
"context"
"log"
"strings"
"github.com/zeromicro/go-zero/core/breaker"
"github.com/zeromicro/go-zero/core/timex"
"go.mongodb.org/mongo-driver/mongo"
mopt "go.mongodb.org/mongo-driver/mongo/options"
)
const (
startSession = "StartSession"
abortTransaction = "AbortTransaction"
commitTransaction = "CommitTransaction"
withTransaction = "WithTransaction"
endSession = "EndSession"
)
type (
// Model is a mongodb store model that represents a collection.
Model struct {
Collection
name string
cli *mongo.Client
brk breaker.Breaker
opts []Option
}
wrappedSession struct {
mongo.Session
name string
brk breaker.Breaker
}
)
// MustNewModel returns a Model, exits on errors.
func MustNewModel(uri, db, collection string, opts ...Option) *Model {
model, err := NewModel(uri, db, collection, opts...)
if err != nil {
log.Fatal(err)
}
return model
}
// NewModel returns a Model.
func NewModel(uri, db, collection string, opts ...Option) (*Model, error) {
cli, err := getClient(uri)
if err != nil {
return nil, err
}
name := strings.Join([]string{uri, collection}, "/")
brk := breaker.GetBreaker(uri)
coll := newCollection(cli.Database(db).Collection(collection), brk)
return newModel(name, cli, coll, brk, opts...), nil
}
func newModel(name string, cli *mongo.Client, coll Collection, brk breaker.Breaker,
opts ...Option) *Model {
return &Model{
name: name,
Collection: coll,
cli: cli,
brk: brk,
opts: opts,
}
}
// StartSession starts a new session.
func (m *Model) StartSession(opts ...*mopt.SessionOptions) (sess mongo.Session, err error) {
err = m.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
logDuration(context.Background(), m.name, startSession, starTime, err)
}()
session, sessionErr := m.cli.StartSession(opts...)
if sessionErr != nil {
return sessionErr
}
sess = &wrappedSession{
Session: session,
name: m.name,
brk: m.brk,
}
return nil
}, acceptable)
return
}
// Aggregate executes an aggregation pipeline.
func (m *Model) Aggregate(ctx context.Context, v, pipeline interface{}, opts ...*mopt.AggregateOptions) error {
cur, err := m.Collection.Aggregate(ctx, pipeline, opts...)
if err != nil {
return err
}
defer cur.Close(ctx)
return cur.All(ctx, v)
}
// DeleteMany deletes documents that match the filter.
func (m *Model) DeleteMany(ctx context.Context, filter interface{}, opts ...*mopt.DeleteOptions) (int64, error) {
res, err := m.Collection.DeleteMany(ctx, filter, opts...)
if err != nil {
return 0, err
}
return res.DeletedCount, nil
}
// DeleteOne deletes the first document that matches the filter.
func (m *Model) DeleteOne(ctx context.Context, filter interface{}, opts ...*mopt.DeleteOptions) (int64, error) {
res, err := m.Collection.DeleteOne(ctx, filter, opts...)
if err != nil {
return 0, err
}
return res.DeletedCount, nil
}
// Find finds documents that match the filter.
func (m *Model) Find(ctx context.Context, v, filter interface{}, opts ...*mopt.FindOptions) error {
cur, err := m.Collection.Find(ctx, filter, opts...)
if err != nil {
return err
}
defer cur.Close(ctx)
return cur.All(ctx, v)
}
// FindOne finds the first document that matches the filter.
func (m *Model) FindOne(ctx context.Context, v, filter interface{}, opts ...*mopt.FindOneOptions) error {
res, err := m.Collection.FindOne(ctx, filter, opts...)
if err != nil {
return err
}
return res.Decode(v)
}
// FindOneAndDelete finds a single document and deletes it.
func (m *Model) FindOneAndDelete(ctx context.Context, v, filter interface{},
opts ...*mopt.FindOneAndDeleteOptions) error {
res, err := m.Collection.FindOneAndDelete(ctx, filter, opts...)
if err != nil {
return err
}
return res.Decode(v)
}
// FindOneAndReplace finds a single document and replaces it.
func (m *Model) FindOneAndReplace(ctx context.Context, v, filter interface{}, replacement interface{},
opts ...*mopt.FindOneAndReplaceOptions) error {
res, err := m.Collection.FindOneAndReplace(ctx, filter, replacement, opts...)
if err != nil {
return err
}
return res.Decode(v)
}
// FindOneAndUpdate finds a single document and updates it.
func (m *Model) FindOneAndUpdate(ctx context.Context, v, filter interface{}, update interface{},
opts ...*mopt.FindOneAndUpdateOptions) error {
res, err := m.Collection.FindOneAndUpdate(ctx, filter, update, opts...)
if err != nil {
return err
}
return res.Decode(v)
}
// AbortTransaction implements the mongo.Session interface.
func (w *wrappedSession) AbortTransaction(ctx context.Context) (err error) {
ctx, span := startSpan(ctx, abortTransaction)
defer func() {
endSpan(span, err)
}()
return w.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
logDuration(ctx, w.name, abortTransaction, starTime, err)
}()
return w.Session.AbortTransaction(ctx)
}, acceptable)
}
// CommitTransaction implements the mongo.Session interface.
func (w *wrappedSession) CommitTransaction(ctx context.Context) (err error) {
ctx, span := startSpan(ctx, commitTransaction)
defer func() {
endSpan(span, err)
}()
return w.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
logDuration(ctx, w.name, commitTransaction, starTime, err)
}()
return w.Session.CommitTransaction(ctx)
}, acceptable)
}
// WithTransaction implements the mongo.Session interface.
func (w *wrappedSession) WithTransaction(
ctx context.Context,
fn func(sessCtx mongo.SessionContext) (interface{}, error),
opts ...*mopt.TransactionOptions,
) (res interface{}, err error) {
ctx, span := startSpan(ctx, withTransaction)
defer func() {
endSpan(span, err)
}()
err = w.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
logDuration(ctx, w.name, withTransaction, starTime, err)
}()
res, err = w.Session.WithTransaction(ctx, fn, opts...)
return err
}, acceptable)
return
}
// EndSession implements the mongo.Session interface.
func (w *wrappedSession) EndSession(ctx context.Context) {
var err error
ctx, span := startSpan(ctx, endSession)
defer func() {
endSpan(span, err)
}()
err = w.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
logDuration(ctx, w.name, endSession, starTime, err)
}()
w.Session.EndSession(ctx)
return nil
}, acceptable)
}

View File

@@ -0,0 +1,243 @@
package mon
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)
func TestModel_StartSession(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
sess, err := m.StartSession()
assert.Nil(t, err)
defer sess.EndSession(context.Background())
_, err = sess.WithTransaction(context.Background(), func(sessCtx mongo.SessionContext) (interface{}, error) {
_ = sessCtx.StartTransaction()
sessCtx.Client().Database("1")
sessCtx.EndSession(context.Background())
return nil, nil
})
assert.Nil(t, err)
assert.NoError(t, sess.CommitTransaction(context.Background()))
assert.Error(t, sess.AbortTransaction(context.Background()))
})
}
func TestModel_Aggregate(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
find := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.FirstBatch,
bson.D{
{Key: "name", Value: "John"},
})
getMore := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.NextBatch,
bson.D{
{Key: "name", Value: "Mary"},
})
killCursors := mtest.CreateCursorResponse(
0,
"DBName.CollectionName",
mtest.NextBatch)
mt.AddMockResponses(find, getMore, killCursors)
var result []interface{}
err := m.Aggregate(context.Background(), &result, mongo.Pipeline{})
assert.Nil(t, err)
assert.Equal(t, 2, len(result))
assert.Equal(t, "John", result[0].(bson.D).Map()["name"])
assert.Equal(t, "Mary", result[1].(bson.D).Map()["name"])
triggerBreaker(m)
assert.Equal(t, errDummy, m.Aggregate(context.Background(), &result, mongo.Pipeline{}))
})
}
func TestModel_DeleteMany(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
val, err := m.DeleteMany(context.Background(), bson.D{})
assert.Nil(t, err)
assert.Equal(t, int64(1), val)
triggerBreaker(m)
_, err = m.DeleteMany(context.Background(), bson.D{})
assert.Equal(t, errDummy, err)
})
}
func TestModel_DeleteOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
val, err := m.DeleteOne(context.Background(), bson.D{})
assert.Nil(t, err)
assert.Equal(t, int64(1), val)
triggerBreaker(m)
_, err = m.DeleteOne(context.Background(), bson.D{})
assert.Equal(t, errDummy, err)
})
}
func TestModel_Find(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
find := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.FirstBatch,
bson.D{
{Key: "name", Value: "John"},
})
getMore := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.NextBatch,
bson.D{
{Key: "name", Value: "Mary"},
})
killCursors := mtest.CreateCursorResponse(
0,
"DBName.CollectionName",
mtest.NextBatch)
mt.AddMockResponses(find, getMore, killCursors)
var result []interface{}
err := m.Find(context.Background(), &result, bson.D{})
assert.Nil(t, err)
assert.Equal(t, 2, len(result))
assert.Equal(t, "John", result[0].(bson.D).Map()["name"])
assert.Equal(t, "Mary", result[1].(bson.D).Map()["name"])
triggerBreaker(m)
assert.Equal(t, errDummy, m.Find(context.Background(), &result, bson.D{}))
})
}
func TestModel_FindOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
find := mtest.CreateCursorResponse(
1,
"DBName.CollectionName",
mtest.FirstBatch,
bson.D{
{Key: "name", Value: "John"},
})
killCursors := mtest.CreateCursorResponse(
0,
"DBName.CollectionName",
mtest.NextBatch)
mt.AddMockResponses(find, killCursors)
var result bson.D
err := m.FindOne(context.Background(), &result, bson.D{})
assert.Nil(t, err)
assert.Equal(t, "John", result.Map()["name"])
triggerBreaker(m)
assert.Equal(t, errDummy, m.FindOne(context.Background(), &result, bson.D{}))
})
}
func TestModel_FindOneAndDelete(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
}...))
var result bson.D
err := m.FindOneAndDelete(context.Background(), &result, bson.D{})
assert.Nil(t, err)
assert.Equal(t, "John", result.Map()["name"])
triggerBreaker(m)
assert.Equal(t, errDummy, m.FindOneAndDelete(context.Background(), &result, bson.D{}))
})
}
func TestModel_FindOneAndReplace(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
}...))
var result bson.D
err := m.FindOneAndReplace(context.Background(), &result, bson.D{}, bson.D{
{Key: "name", Value: "Mary"},
})
assert.Nil(t, err)
assert.Equal(t, "John", result.Map()["name"])
triggerBreaker(m)
assert.Equal(t, errDummy, m.FindOneAndReplace(context.Background(), &result, bson.D{}, bson.D{
{Key: "name", Value: "Mary"},
}))
})
}
func TestModel_FindOneAndUpdate(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
mt.Run("test", func(mt *mtest.T) {
m := createModel(mt)
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
}...))
var result bson.D
err := m.FindOneAndUpdate(context.Background(), &result, bson.D{}, bson.D{
{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
})
assert.Nil(t, err)
assert.Equal(t, "John", result.Map()["name"])
triggerBreaker(m)
assert.Equal(t, errDummy, m.FindOneAndUpdate(context.Background(), &result, bson.D{}, bson.D{
{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
}))
})
}
func createModel(mt *mtest.T) *Model {
Inject(mt.Name(), mt.Client)
return MustNewModel(mt.Name(), mt.DB.Name(), mt.Coll.Name())
}
func triggerBreaker(m *Model) {
m.Collection.(*decoratedCollection).brk = new(dropBreaker)
}

View File

@@ -0,0 +1,29 @@
package mon
import (
"time"
"github.com/zeromicro/go-zero/core/syncx"
)
var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
type (
options struct {
timeout time.Duration
}
// Option defines the method to customize a mongo model.
Option func(opts *options)
)
// SetSlowThreshold sets the slow threshold.
func SetSlowThreshold(threshold time.Duration) {
slowThreshold.Set(threshold)
}
func defaultOptions() *options {
return &options{
timeout: defaultTimeout,
}
}

View File

@@ -0,0 +1,18 @@
package mon
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSetSlowThreshold(t *testing.T) {
assert.Equal(t, defaultSlowThreshold, slowThreshold.Load())
SetSlowThreshold(time.Second)
assert.Equal(t, time.Second, slowThreshold.Load())
}
func TestDefaultOptions(t *testing.T) {
assert.Equal(t, defaultTimeout, defaultOptions().timeout)
}

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