Compare commits

..

311 Commits

Author SHA1 Message Date
Kevin Wan
06e4914e41 feat: add logger.WithFields (#2546) 2022-10-22 23:28:34 +08:00
Kevin Wan
9cadab2684 chore: refactor (#2545)
* chore: refactor

* chore: refactor
2022-10-22 22:52:40 +08:00
chen quan
7fe2492009 feat(trace): support for disabling tracing of specified spanName (#2363) 2022-10-22 22:14:12 +08:00
chen quan
22bdf0bbd5 chore: adjust rpc comment format (#2501) 2022-10-22 22:07:55 +08:00
chowyu12
c92a2d1b77 feat: remove info log when disable log (#2525)
* add go-grpc_opt and go_opt for grpc new command

* feat: remove log when disable log

Co-authored-by: zhouyy <zhouyy@ickey.cn>
2022-10-22 22:07:17 +08:00
swliao425
b21162d638 fix: redis's pipeline logs are not printed completely (#2538)
* fix: redis's pipeline logs are not printed completely

* add unit test

Signed-off-by: liaoshiwei <liaoshiwei@uniontech.com>

Signed-off-by: liaoshiwei <liaoshiwei@uniontech.com>
2022-10-22 21:57:40 +08:00
anqiansong
7c9ef3ca67 fix(goctl): Fix issues (#2543)
* fix #2541

* fix #2432

* Fix review comment

* foramt code

* foramt code
2022-10-22 21:01:15 +08:00
chen quan
bbadbe0175 chore(action): upgrade action (#2521)
- codecov/codecov-action
- actions/setup-go
- usthe/issues-translate-action(origin:omsun28/issues-translate-action)
2022-10-22 19:06:53 +08:00
Kevin Wan
f9beab1095 feat: support uuid.UUID in mapping (#2537) 2022-10-20 20:11:19 +08:00
Kevin Wan
de5c59aad3 chore: add more tests (#2536) 2022-10-19 20:39:46 +08:00
Gang Wu
36d3765c5c Fix typo (#2531) 2022-10-18 17:27:06 +08:00
dependabot[bot]
d326e6f813 chore(deps): bump google.golang.org/grpc from 1.50.0 to 1.50.1 (#2527) 2022-10-18 17:02:11 +08:00
wuleiming2009
ea52fe2e0d Fix the wrong key about FindOne in mongo of goctl. (#2523) 2022-10-17 19:58:57 +08:00
Kevin Wan
05a5de7c6d chore: fix lint errors (#2520) 2022-10-17 06:30:58 +08:00
Kevin Wan
d4c9fd2aff chore: add golangci-lint config file (#2519)
* chore: add golangci-lint config file

* chore: member alignment
2022-10-14 22:45:48 +08:00
dependabot[bot]
776673d57d chore(deps): bump go.opentelemetry.io/otel/exporters/jaeger (#2514)
Bumps [go.opentelemetry.io/otel/exporters/jaeger](https://github.com/open-telemetry/opentelemetry-go) from 1.10.0 to 1.11.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.10.0...v1.11.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>
Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-10-14 12:42:15 +08:00
anqiansong
1b87f5e30d Fix mongo insert tpl (#2512) 2022-10-14 12:27:04 +08:00
dependabot[bot]
bc47959384 chore(deps): bump go.opentelemetry.io/otel/exporters/zipkin (#2511)
Bumps [go.opentelemetry.io/otel/exporters/zipkin](https://github.com/open-telemetry/opentelemetry-go) from 1.10.0 to 1.11.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.10.0...v1.11.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-10-14 11:47:38 +08:00
dependabot[bot]
9f6d926455 chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc (#2510)
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) from 1.10.0 to 1.11.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.10.0...v1.11.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-10-13 23:13:04 +08:00
foliet
f7a4e3a19e chore: fix naming problem (#2500)
When I was looking for how to mock mongo client, I found some naming problems and wanted to fix them.
2022-10-13 22:53:27 +08:00
swliao425
a515a3c735 chore: sqlx's metric name is different from redis (#2505) 2022-10-13 22:52:36 +08:00
dependabot[bot]
6f6f1ae21f chore(deps): bump go.opentelemetry.io/otel/sdk from 1.10.0 to 1.11.0 (#2504)
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.10.0 to 1.11.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.10.0...v1.11.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-10-13 22:51:40 +08:00
Kevin Wan
10f94ffcc2 chore: remove unnecessary code (#2499) 2022-10-11 22:56:12 +08:00
sado
f068062b13 token limit support context (#2335)
* token limit support context

* add token limit with ctx

add token limit with ctx

Co-authored-by: sado <liaoyonglin@bilibili.com>
2022-10-11 22:40:00 +08:00
foliet
799c118d95 feat(goctl): better generate the api code of typescript (#2483) 2022-10-11 22:19:22 +08:00
#Suyghur
74cc6b55e8 fix: replace Infof() with Errorf() in DurationInterceptor (#2495) (#2497) 2022-10-11 21:45:31 +08:00
cui fliter
fc59aec2e7 fix a few function names on comments (#2496)
Signed-off-by: cui fliter <imcusg@gmail.com>

Signed-off-by: cui fliter <imcusg@gmail.com>
2022-10-10 22:12:11 +08:00
dependabot[bot]
7868667b4f chore(deps): bump google.golang.org/grpc from 1.49.0 to 1.50.0 (#2487)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.49.0 to 1.50.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.49.0...v1.50.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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-07 22:42:24 +08:00
Kevin Wan
773b59106b chore: remove init if possible (#2485) 2022-10-06 23:57:56 +08:00
dependabot[bot]
97f8667b71 chore(deps): bump go.mongodb.org/mongo-driver from 1.10.2 to 1.10.3 (#2484)
Bumps [go.mongodb.org/mongo-driver](https://github.com/mongodb/mongo-go-driver) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/mongodb/mongo-go-driver/releases)
- [Commits](https://github.com/mongodb/mongo-go-driver/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: go.mongodb.org/mongo-driver
  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-10-06 11:04:11 +08:00
foliet
b51339b69b fix(mongo): fix file name generation errors (#2479)
Before this, no matter what style is used, lowercase file names without underscores will be generated.
2022-10-04 18:09:03 +08:00
Kevin Wan
38a73d7fbe fix: etcd reconnecting problem (#2478) 2022-10-02 22:03:56 +08:00
re-dylan
e50689beed fix #2343 (#2349)
Co-authored-by: dylan.wang <dylan.wang@yijinin.com>
2022-10-02 21:46:33 +08:00
Kevin Wan
1bc138bd34 chore: refactor to reduce duplicated code (#2477) 2022-10-01 21:45:53 +08:00
Kevin Wan
4b9066eda6 chore: better shedding algorithm, make sure recover from shedding (#2476)
* backup

* chore: better shedding algorithm, make sure recover from shedding
2022-10-01 20:55:25 +08:00
#Suyghur
0c66e041b5 feat(redis):add timeout method to extend blpop (#2472) 2022-10-01 20:53:54 +08:00
Halo
aa2be0163a fix: add more tests (#2473)
* chore: add string to map in httpx parse method

* feat: add httpx parse stringToMap method test

* fix: add more test
2022-09-30 22:01:39 +08:00
Kevin Wan
ada2941e87 chore: sort methods (#2470) 2022-09-30 14:57:40 +08:00
Kevin Wan
59c0013cd1 feat: add logc package, support AddGlobalFields for both logc and logx. (#2463)
* feat: add logc package

* feat: add logc, add AddGlobalFields for both logc and logx

* chore: add benchmarks

* chore: add more tests

* chore: simplify globalFields in logx

* chore: remove outdated comments
2022-09-29 22:49:41 +08:00
Halo
05737f6519 feat: add string to map in httpx parse method (#2459)
* chore: add string to map in httpx parse method

* feat: add httpx parse stringToMap method test
2022-09-29 22:34:58 +08:00
chen quan
4f6a900fd4 fix(goctl): fix the unit test bug of goctl (#2458) 2022-09-27 23:52:05 +08:00
aV
63cfe60f1a Readme Tweak (#2436)
* Update readme.md

* Update readme.md

* Update readme.md

Co-authored-by: Kevin Wan <wanjunfeng@gmail.com>
2022-09-25 22:59:55 +08:00
bensonfx
e7acadb15d fix #2435 (#2442)
* feat: add color to debug (#2433)

* fix header and path type ts gen

Co-authored-by: chen quan <chenquan.dev@gmail.com>
2022-09-24 22:28:25 +08:00
chen quan
111e626a73 refactor: adjust http request slow log format (#2440) 2022-09-23 21:20:38 +08:00
Kevin Wan
1a6d7b3ef6 chore: gofumpt (#2439) 2022-09-22 22:40:01 +08:00
chen quan
2e1e4f3574 feat: add color to debug (#2433) 2022-09-21 22:30:06 +08:00
Kevin Wan
22d0a2120a chore: replace fmt.Fprint (#2425) 2022-09-20 23:51:58 +08:00
chen quan
68e15360c2 fix: fix log output (#2424) 2022-09-20 22:45:52 +08:00
jesse.tang
1b344a8851 cleanup: deprecated field and func (#2416)
* cleanup: deprecated field and func

* fmt import order
2022-09-20 22:13:34 +08:00
dawn_zhou
d640544a40 refactor: redis error for prometheus metric label (#2412)
Co-authored-by: dawn.zhou <dawn.zhou@yijinin.com>
2022-09-20 21:13:33 +08:00
MarkJoyMa
e6aa6fc361 feat: add log debug level (#2411) 2022-09-20 07:50:11 +08:00
MarkJoyMa
4c927624b0 fix goctl help message (#2414) 2022-09-19 14:05:46 +08:00
Kevin Wan
0ea92b7280 chore: add more tests (#2410) 2022-09-19 13:52:14 +08:00
anqiansong
2cde970c9e feat(goctl):Add ignore-columns flag (#2407)
* fix #2074,#2100

* format code

* fix #2397

* format code

* Support comma spliter

* format code
2022-09-19 11:49:39 +08:00
Kevin Wan
5061158bd6 chore: add more tests (#2409) 2022-09-18 23:17:21 +08:00
kevin
9138056c01 chore: update go-zero to v1.4.1 2022-09-17 23:07:29 +08:00
Kevin Wan
0b1884b6bd feat: support caller skip in logx (#2401)
* feat: support caller skip in logx

* chore: remove debug prints

* chore: remove debug prints

* 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>

* chore: simplify test code

* chore: remove new WithFields in logx, and deprecated old WithFields

* chore: simplify WithDuration

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-17 22:48:24 +08:00
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
427 changed files with 17768 additions and 7266 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
issuehunt: # Replace with a single IssueHunt 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:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -12,13 +12,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ^1.15
go-version: ^1.16
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Get dependencies
run: |
@@ -34,19 +34,19 @@ jobs:
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Codecov
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3
test-win:
name: Windows
runs-on: windows-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ^1.15
go-version: ^1.16
- name: Checkout codebase
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Test
run: |

View File

@@ -9,7 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: tomsun28/issues-translate-action@v2.6
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: true
# not require, default false, . Decide whether to modify the issue title

View File

@@ -7,10 +7,10 @@ jobs:
close-issues:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
- uses: actions/stale@v6
with:
days-before-issue-stale: 30
days-before-issue-close: 14
days-before-issue-stale: 365
days-before-issue-close: 90
stale-issue-label: "stale"
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."

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
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: reviewdog/action-staticcheck@v1
with:
github_token: ${{ secrets.github_token }}

2
.gitignore vendored
View File

@@ -18,9 +18,11 @@
# for test purpose
**/adhoc
go.work
go.work.sum
# gitlab ci
.cache
.golangci.yml
# vim auto backup file
*~

View File

@@ -22,7 +22,7 @@ We hope that the items listed below will inspire further engagement from the com
## 2022
- [x] Support `context` in redis 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.
- [ ] Support `goctl doctor` command to report potential issues for given service
- [ ] 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 {
return false, err
}
if !isSet {
return false, nil
}
return true, nil
return isSet, nil
}
func (f *Filter) getLocations(data []byte) []uint {

View File

@@ -5,12 +5,12 @@ import (
"fmt"
"strings"
"sync"
"time"
"github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/core/timex"
)
const (
@@ -198,7 +198,7 @@ type errorWindow struct {
func (ew *errorWindow) add(reason string) {
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.count = mathx.MinInt(ew.count+1, numHistoryReasons)
ew.lock.Unlock()

View File

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

View File

@@ -26,7 +26,7 @@ type (
// CacheOption defines the method to customize a Cache.
CacheOption func(cache *Cache)
// A Cache object is a in-memory cache.
// A Cache object is an in-memory cache.
Cache struct {
name string
lock sync.Mutex

View File

@@ -68,6 +68,24 @@ func (m *SafeMap) Get(key interface{}) (interface{}, bool) {
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.
func (m *SafeMap) Set(key, value interface{}) {
m.lock.Lock()

View File

@@ -1,6 +1,7 @@
package collection
import (
"sync/atomic"
"testing"
"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)
}

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 (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"strings"
"github.com/zeromicro/go-zero/core/mapping"
)
var loaders = map[string]func([]byte, interface{}) error{
".json": LoadConfigFromJsonBytes,
".yaml": LoadConfigFromYamlBytes,
".yml": LoadConfigFromYamlBytes,
".json": LoadFromJsonBytes,
".toml": LoadFromTomlBytes,
".yaml": LoadFromYamlBytes,
".yml": LoadFromYamlBytes,
}
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
func LoadConfig(file string, v interface{}, opts ...Option) error {
content, err := ioutil.ReadFile(file)
// Load loads config into v from file, .json, .yaml and .yml are acceptable.
func Load(file string, v interface{}, opts ...Option) error {
content, err := os.ReadFile(file)
if err != nil {
return err
}
loader, ok := loaders[path.Ext(file)]
loader, ok := loaders[strings.ToLower(path.Ext(file))]
if !ok {
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)
}
// LoadConfigFromJsonBytes loads config into v from content json bytes.
func LoadConfigFromJsonBytes(content []byte, v interface{}) error {
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
// 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)
}
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
func LoadConfigFromYamlBytes(content []byte, v interface{}) error {
// LoadConfigFromJsonBytes loads config into v from content json bytes.
// 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)
}
// 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.
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())
}
}

View File

@@ -1,7 +1,6 @@
package conf
import (
"io/ioutil"
"os"
"testing"
@@ -11,14 +10,14 @@ import (
)
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) {
filename, err := fs.TempFilenameWithText("hello")
assert.Nil(t, err)
defer os.Remove(filename)
assert.NotNil(t, LoadConfig(filename, nil))
assert.NotNil(t, Load(filename, nil))
}
func TestConfigJson(t *testing.T) {
@@ -57,6 +56,57 @@ 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) {
tests := []string{
".json",
@@ -94,12 +144,12 @@ func TestConfigJsonEnv(t *testing.T) {
}
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 {
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
}

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 (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
"sync"
)
@@ -37,7 +37,7 @@ func AddTLS(endpoints []string, certFile, certKeyFile, caFile string, insecureSk
return err
}
caData, err := ioutil.ReadFile(caFile)
caData, err := os.ReadFile(caFile)
if err != nil {
return err
}

View File

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

View File

@@ -11,6 +11,7 @@ import (
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stringx"
"go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
)
@@ -112,6 +113,7 @@ func TestCluster_Load(t *testing.T) {
restore := setMockClient(cli)
defer restore()
cli.EXPECT().Get(gomock.Any(), "any/", gomock.Any()).Return(&clientv3.GetResponse{
Header: &etcdserverpb.ResponseHeader{},
Kvs: []*mvccpb.KeyValue{
{
Key: []byte("hello"),
@@ -168,7 +170,7 @@ func TestCluster_Watch(t *testing.T) {
listener.EXPECT().OnDelete(gomock.Any()).Do(func(_ interface{}) {
wg.Done()
}).MaxTimes(1)
go c.watch(cli, "any")
go c.watch(cli, "any", 0)
ch <- clientv3.WatchResponse{
Events: []*clientv3.Event{
{
@@ -212,7 +214,7 @@ func TestClusterWatch_RespFailures(t *testing.T) {
ch <- resp
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(c.done)
}()
c.watch(cli, "any")
c.watch(cli, "any", 0)
}
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
import (
"io/ioutil"
"os"
"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,
// and remove the file by name.
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 {
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
}

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

View File

@@ -4,7 +4,6 @@ import (
"bufio"
"bytes"
"io"
"io/ioutil"
"os"
"strings"
)
@@ -26,7 +25,7 @@ type (
func DupReadCloser(reader io.ReadCloser) (io.ReadCloser, io.ReadCloser) {
var buf bytes.Buffer
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.
@@ -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.
func ReadText(filename string) (string, error) {
content, err := ioutil.ReadFile(filename)
content, err := os.ReadFile(filename)
if err != nil {
return "", err
}

View File

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

View File

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

View File

@@ -13,6 +13,16 @@ func Marshal(v interface{}) ([]byte, error) {
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.
func Unmarshal(data []byte, v interface{}) error {
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
import (
"context"
"errors"
"strconv"
"time"
@@ -74,7 +75,12 @@ func NewPeriodLimit(period, quota int, limitStore *redis.Redis, keyPrefix string
// Take requests a permit, it returns the permit state.
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.calcExpireSeconds()),
})

View File

@@ -1,6 +1,8 @@
package limit
import (
"context"
"errors"
"fmt"
"strconv"
"sync"
@@ -58,8 +60,8 @@ type TokenLimiter struct {
timestampKey string
rescueLock sync.Mutex
redisAlive uint32
rescueLimiter *xrate.Limiter
monitorStarted bool
rescueLimiter *xrate.Limiter
}
// NewTokenLimiter returns a new TokenLimiter that allows events up to rate and permits
@@ -84,19 +86,31 @@ func (lim *TokenLimiter) Allow() bool {
return lim.AllowN(time.Now(), 1)
}
// AllowCtx is shorthand for AllowNCtx(ctx,time.Now(), 1) with incoming context.
func (lim *TokenLimiter) AllowCtx(ctx context.Context) bool {
return lim.AllowNCtx(ctx, time.Now(), 1)
}
// AllowN reports whether n events may happen at time now.
// Use this method if you intend to drop / skip events that exceed the rate.
// Otherwise, use Reserve or Wait.
func (lim *TokenLimiter) AllowN(now time.Time, n int) bool {
return lim.reserveN(now, n)
return lim.reserveN(context.Background(), now, n)
}
func (lim *TokenLimiter) reserveN(now time.Time, n int) bool {
// AllowNCtx reports whether n events may happen at time now with incoming context.
// Use this method if you intend to drop / skip events that exceed the rate.
// Otherwise, use Reserve or Wait.
func (lim *TokenLimiter) AllowNCtx(ctx context.Context, now time.Time, n int) bool {
return lim.reserveN(ctx, now, n)
}
func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) bool {
if atomic.LoadUint32(&lim.redisAlive) == 0 {
return lim.rescueLimiter.AllowN(now, n)
}
resp, err := lim.store.Eval(
resp, err := lim.store.EvalCtx(ctx,
script,
[]string{
lim.tokenKey,
@@ -113,6 +127,10 @@ func (lim *TokenLimiter) reserveN(now time.Time, n int) bool {
if err == redis.Nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
logx.Errorf("fail to use rate limiter: %s", err)
return false
}
if err != nil {
logx.Errorf("fail to use rate limiter: %s, use in-process limiter for rescue", err)
lim.startMonitor()

View File

@@ -1,6 +1,7 @@
package limit
import (
"context"
"testing"
"time"
@@ -15,6 +16,30 @@ func init() {
logx.Disable()
}
func TestTokenLimit_WithCtx(t *testing.T) {
s, err := miniredis.Run()
assert.Nil(t, err)
const (
total = 100
rate = 5
burst = 10
)
l := NewTokenLimiter(rate, burst, redis.New(s.Addr()), "tokenlimit")
defer s.Close()
ctx, cancel := context.WithCancel(context.Background())
ok := l.AllowCtx(ctx)
assert.True(t, ok)
cancel()
for i := 0; i < total; i++ {
ok := l.AllowCtx(ctx)
assert.False(t, ok)
assert.False(t, l.monitorStarted)
}
}
func TestTokenLimit_Rescue(t *testing.T) {
s, err := miniredis.Run()
assert.Nil(t, err)

View File

@@ -17,7 +17,7 @@ import (
const (
defaultBuckets = 50
defaultWindow = time.Second * 5
// using 1000m notation, 900m is like 80%, keep it as var for unit test
// using 1000m notation, 900m is like 90%, keep it as var for unit test
defaultCpuThreshold = 900
defaultMinRt = float64(time.Second / time.Millisecond)
// moving average hyperparameter beta for calculating requests on the fly
@@ -70,7 +70,7 @@ type (
flying int64
avgFlying float64
avgFlyingLock syncx.SpinLock
dropTime *syncx.AtomicDuration
overloadTime *syncx.AtomicDuration
droppedRecently *syncx.AtomicBool
passCounter *collection.RollingWindow
rtCounter *collection.RollingWindow
@@ -106,7 +106,7 @@ func NewAdaptiveShedder(opts ...ShedderOption) Shedder {
return &adaptiveShedder{
cpuThreshold: options.cpuThreshold,
windows: int64(time.Second / bucketDuration),
dropTime: syncx.NewAtomicDuration(),
overloadTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.NewAtomicBool(),
passCounter: collection.NewRollingWindow(options.buckets, bucketDuration,
collection.IgnoreCurrentBucket()),
@@ -118,7 +118,6 @@ func NewAdaptiveShedder(opts ...ShedderOption) Shedder {
// Allow implements Shedder.Allow.
func (as *adaptiveShedder) Allow() (Promise, error) {
if as.shouldDrop() {
as.dropTime.Set(timex.Now())
as.droppedRecently.Set(true)
return nil, ErrServiceOverloaded
@@ -215,21 +214,26 @@ func (as *adaptiveShedder) stillHot() bool {
return false
}
dropTime := as.dropTime.Load()
if dropTime == 0 {
overloadTime := as.overloadTime.Load()
if overloadTime == 0 {
return false
}
hot := timex.Since(dropTime) < coolOffDuration
if !hot {
as.droppedRecently.Set(false)
if timex.Since(overloadTime) < coolOffDuration {
return true
}
return hot
as.droppedRecently.Set(false)
return false
}
func (as *adaptiveShedder) systemOverloaded() bool {
return systemOverloadChecker(as.cpuThreshold)
if !systemOverloadChecker(as.cpuThreshold) {
return false
}
as.overloadTime.Set(timex.Now())
return true
}
// WithBuckets customizes the Shedder with given number of buckets.

View File

@@ -13,6 +13,7 @@ import (
"github.com/zeromicro/go-zero/core/mathx"
"github.com/zeromicro/go-zero/core/stat"
"github.com/zeromicro/go-zero/core/syncx"
"github.com/zeromicro/go-zero/core/timex"
)
const (
@@ -136,7 +137,7 @@ func TestAdaptiveShedderShouldDrop(t *testing.T) {
passCounter: passCounter,
rtCounter: rtCounter,
windows: buckets,
dropTime: syncx.NewAtomicDuration(),
overloadTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.NewAtomicBool(),
}
// cpu >= 800, inflight < maxPass
@@ -190,12 +191,15 @@ func TestAdaptiveShedderStillHot(t *testing.T) {
passCounter: passCounter,
rtCounter: rtCounter,
windows: buckets,
dropTime: syncx.NewAtomicDuration(),
overloadTime: syncx.NewAtomicDuration(),
droppedRecently: syncx.ForAtomicBool(true),
}
assert.False(t, shedder.stillHot())
shedder.dropTime.Set(-coolOffDuration * 2)
shedder.overloadTime.Set(-coolOffDuration * 2)
assert.False(t, shedder.stillHot())
shedder.droppedRecently.Set(true)
shedder.overloadTime.Set(timex.Now())
assert.True(t, shedder.stillHot())
}
func BenchmarkAdaptiveShedder_Allow(b *testing.B) {

122
core/logc/logs.go Normal file
View File

@@ -0,0 +1,122 @@
package logc
import (
"context"
"fmt"
"github.com/zeromicro/go-zero/core/logx"
)
type (
LogConf = logx.LogConf
LogField = logx.LogField
)
// AddGlobalFields adds global fields.
func AddGlobalFields(fields ...LogField) {
logx.AddGlobalFields(fields...)
}
// Alert alerts v in alert level, and the message is written to error log.
func Alert(_ context.Context, v string) {
logx.Alert(v)
}
// Close closes the logging.
func Close() error {
return logx.Close()
}
// Error writes v into error log.
func Error(ctx context.Context, v ...interface{}) {
getLogger(ctx).Error(v...)
}
// Errorf writes v with format into error log.
func Errorf(ctx context.Context, format string, v ...interface{}) {
getLogger(ctx).Errorf(fmt.Errorf(format, v...).Error())
}
// Errorv writes v into error log with json content.
// No call stack attached, because not elegant to pack the messages.
func Errorv(ctx context.Context, v interface{}) {
getLogger(ctx).Errorv(v)
}
// Errorw writes msg along with fields into error log.
func Errorw(ctx context.Context, msg string, fields ...LogField) {
getLogger(ctx).Errorw(msg, fields...)
}
// Field returns a LogField for the given key and value.
func Field(key string, value interface{}) LogField {
return logx.Field(key, value)
}
// Info writes v into access log.
func Info(ctx context.Context, v ...interface{}) {
getLogger(ctx).Info(v...)
}
// Infof writes v with format into access log.
func Infof(ctx context.Context, format string, v ...interface{}) {
getLogger(ctx).Infof(format, v...)
}
// Infov writes v into access log with json content.
func Infov(ctx context.Context, v interface{}) {
getLogger(ctx).Infov(v)
}
// Infow writes msg along with fields into access log.
func Infow(ctx context.Context, msg string, fields ...LogField) {
getLogger(ctx).Infow(msg, fields...)
}
// Must checks if err is nil, otherwise logs the error and exits.
func Must(err error) {
logx.Must(err)
}
// MustSetup sets up logging with given config c. It exits on error.
func MustSetup(c logx.LogConf) {
logx.MustSetup(c)
}
// SetLevel sets the logging level. It can be used to suppress some logs.
func SetLevel(level uint32) {
logx.SetLevel(level)
}
// 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 {
return logx.SetUp(c)
}
// Slow writes v into slow log.
func Slow(ctx context.Context, v ...interface{}) {
getLogger(ctx).Slow(v...)
}
// Slowf writes v with format into slow log.
func Slowf(ctx context.Context, format string, v ...interface{}) {
getLogger(ctx).Slowf(format, v...)
}
// Slowv writes v into slow log with json content.
func Slowv(ctx context.Context, v interface{}) {
getLogger(ctx).Slowv(v)
}
// Sloww writes msg along with fields into slow log.
func Sloww(ctx context.Context, msg string, fields ...LogField) {
getLogger(ctx).Sloww(msg, fields...)
}
// getLogger returns the logx.Logger with the given ctx and correct caller.
func getLogger(ctx context.Context) logx.Logger {
return logx.WithContext(ctx).WithCallerSkip(1)
}

218
core/logc/logs_test.go Normal file
View File

@@ -0,0 +1,218 @@
package logc
import (
"bytes"
"context"
"encoding/json"
"fmt"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
)
func TestAddGlobalFields(t *testing.T) {
var buf bytes.Buffer
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
Info(context.Background(), "hello")
buf.Reset()
AddGlobalFields(Field("a", "1"), Field("b", "2"))
AddGlobalFields(Field("c", "3"))
Info(context.Background(), "world")
var m map[string]interface{}
assert.NoError(t, json.Unmarshal(buf.Bytes(), &m))
assert.Equal(t, "1", m["a"])
assert.Equal(t, "2", m["b"])
assert.Equal(t, "3", m["c"])
}
func TestAlert(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
Alert(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), "foo"), buf.String())
}
func TestError(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Error(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorf(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Errorf(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorv(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Errorv(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestErrorw(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Errorw(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfo(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Info(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfof(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Infof(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfov(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Infov(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestInfow(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Infow(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)))
}
func TestMust(t *testing.T) {
assert.NotPanics(t, func() {
Must(nil)
})
assert.NotPanics(t, func() {
MustSetup(LogConf{})
})
}
func TestMisc(t *testing.T) {
SetLevel(logx.DebugLevel)
assert.NoError(t, SetUp(LogConf{}))
assert.NoError(t, Close())
}
func TestSlow(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Slow(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSlowf(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Slowf(context.Background(), "foo %s", "bar")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSlowv(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Slowv(context.Background(), "foo")
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func TestSloww(t *testing.T) {
var buf strings.Builder
writer := logx.NewWriter(&buf)
old := logx.Reset()
logx.SetWriter(writer)
defer logx.SetWriter(old)
file, line := getFileLine()
Sloww(context.Background(), "foo", Field("a", "b"))
assert.True(t, strings.Contains(buf.String(), fmt.Sprintf("%s:%d", file, line+1)), buf.String())
}
func getFileLine() (string, int) {
_, file, line, _ := runtime.Caller(1)
short := file
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
return short, line
}

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

@@ -7,8 +7,20 @@ type LogConf struct {
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]"`
Level string `json:",default=info,options=[debug,info,error,severe]"`
Compress bool `json:",optional"`
KeepDays int `json:",optional"`
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]"`
}

View File

@@ -1,94 +0,0 @@
package logx
import (
"fmt"
"io"
"sync/atomic"
"time"
"github.com/zeromicro/go-zero/core/timex"
)
const durationCallerDepth = 3
type durationLogger logEntry
// WithDuration returns a Logger which logs the given duration.
func WithDuration(d time.Duration) Logger {
return &durationLogger{
Duration: timex.ReprOfDuration(d),
}
}
func (l *durationLogger) Error(v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(errorLog, levelError, formatWithCaller(fmt.Sprint(v...), durationCallerDepth))
}
}
func (l *durationLogger) Errorf(format string, v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(errorLog, levelError, formatWithCaller(fmt.Sprintf(format, v...), durationCallerDepth))
}
}
func (l *durationLogger) Errorv(v interface{}) {
if shallLog(ErrorLevel) {
l.write(errorLog, levelError, v)
}
}
func (l *durationLogger) Info(v ...interface{}) {
if shallLog(InfoLevel) {
l.write(infoLog, levelInfo, fmt.Sprint(v...))
}
}
func (l *durationLogger) Infof(format string, v ...interface{}) {
if shallLog(InfoLevel) {
l.write(infoLog, levelInfo, fmt.Sprintf(format, v...))
}
}
func (l *durationLogger) Infov(v interface{}) {
if shallLog(InfoLevel) {
l.write(infoLog, levelInfo, v)
}
}
func (l *durationLogger) Slow(v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(slowLog, levelSlow, fmt.Sprint(v...))
}
}
func (l *durationLogger) Slowf(format string, v ...interface{}) {
if shallLog(ErrorLevel) {
l.write(slowLog, levelSlow, fmt.Sprintf(format, v...))
}
}
func (l *durationLogger) Slowv(v interface{}) {
if shallLog(ErrorLevel) {
l.write(slowLog, levelSlow, v)
}
}
func (l *durationLogger) WithDuration(duration time.Duration) Logger {
l.Duration = timex.ReprOfDuration(duration)
return l
}
func (l *durationLogger) write(writer io.Writer, level string, val interface{}) {
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
writePlainAny(writer, level, val, l.Duration)
default:
outputJson(writer, &durationLogger{
Timestamp: getTimestamp(),
Level: level,
Content: val,
Duration: l.Duration,
})
}
}

View File

@@ -1,87 +0,0 @@
package logx
import (
"log"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestWithDurationError(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Error("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationErrorf(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Errorf("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationErrorv(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Errorv("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationInfo(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Info("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationInfoConsole(t *testing.T) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType)
defer func() {
atomic.StoreUint32(&encoding, old)
}()
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Info("foo")
assert.True(t, strings.Contains(builder.String(), "ms"), builder.String())
}
func TestWithDurationInfof(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Infof("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationInfov(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Infov("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationSlow(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).Slow("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationSlowf(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).WithDuration(time.Hour).Slowf("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}
func TestWithDurationSlowv(t *testing.T) {
var builder strings.Builder
log.SetOutput(&builder)
WithDuration(time.Second).WithDuration(time.Hour).Slowv("foo")
assert.True(t, strings.Contains(builder.String(), "duration"), builder.String())
}

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

@@ -0,0 +1,45 @@
package logx
import (
"context"
"sync"
"sync/atomic"
)
var (
fieldsContextKey contextKey
globalFields atomic.Value
globalFieldsLock sync.Mutex
)
type contextKey struct{}
// AddGlobalFields adds global fields.
func AddGlobalFields(fields ...LogField) {
globalFieldsLock.Lock()
defer globalFieldsLock.Unlock()
old := globalFields.Load()
if old == nil {
globalFields.Store(append([]LogField(nil), fields...))
} else {
globalFields.Store(append(old.([]LogField), fields...))
}
}
// ContextWithFields returns a new context with the given fields.
func ContextWithFields(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)
}
// WithFields returns a new logger with the given fields.
// deprecated: use ContextWithFields instead.
func WithFields(ctx context.Context, fields ...LogField) context.Context {
return ContextWithFields(ctx, fields...)
}

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

@@ -0,0 +1,104 @@
package logx
import (
"bytes"
"context"
"encoding/json"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddGlobalFields(t *testing.T) {
var buf bytes.Buffer
writer := NewWriter(&buf)
old := Reset()
SetWriter(writer)
defer SetWriter(old)
Info("hello")
buf.Reset()
AddGlobalFields(Field("a", "1"), Field("b", "2"))
AddGlobalFields(Field("c", "3"))
Info("world")
var m map[string]interface{}
assert.NoError(t, json.Unmarshal(buf.Bytes(), &m))
assert.Equal(t, "1", m["a"])
assert.Equal(t, "2", m["b"])
assert.Equal(t, "3", m["c"])
}
func TestContextWithFields(t *testing.T) {
ctx := ContextWithFields(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 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 = ContextWithFields(ctx, Field("a", 1), Field("b", 2))
ctx = ContextWithFields(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)
}
func BenchmarkAtomicValue(b *testing.B) {
b.ReportAllocs()
var container atomic.Value
vals := []LogField{
Field("a", "b"),
Field("c", "d"),
Field("e", "f"),
}
container.Store(&vals)
for i := 0; i < b.N; i++ {
val := container.Load()
if val != nil {
_ = *val.(*[]LogField)
}
}
}
func BenchmarkRWMutex(b *testing.B) {
b.ReportAllocs()
var lock sync.RWMutex
vals := []LogField{
Field("a", "b"),
Field("c", "d"),
Field("e", "f"),
}
for i := 0; i < b.N; i++ {
lock.RLock()
_ = vals
lock.RUnlock()
}
}

View File

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

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

@@ -0,0 +1,50 @@
package logx
import (
"context"
"time"
)
// A Logger represents a logger.
type Logger interface {
// Debug logs a message at info level.
Debug(...interface{})
// Debugf logs a message at info level.
Debugf(string, ...interface{})
// Debugv logs a message at info level.
Debugv(interface{})
// Debugw logs a message at info level.
Debugw(string, ...LogField)
// 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)
// WithCallerSkip returns a new logger with the given caller skip.
WithCallerSkip(skip int) Logger
// WithContext returns a new logger with the given context.
WithContext(ctx context.Context) Logger
// WithDuration returns a new logger with the given duration.
WithDuration(d time.Duration) Logger
// WithFields returns a new logger with the given fields.
WithFields(fields ...LogField) Logger
}

View File

@@ -1,220 +1,93 @@
package logx
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/zeromicro/go-zero/core/iox"
"github.com/zeromicro/go-zero/core/sysx"
"github.com/zeromicro/go-zero/core/timex"
)
const (
// 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
)
const callerDepth = 4
var (
// ErrLogPathNotSet is an error that indicates the log path is not set.
ErrLogPathNotSet = errors.New("log path must be set")
// ErrLogNotInitialized is an error that log is not initialized.
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
timeFormat = "2006-01-02T15:04:05.000Z07:00"
logLevel uint32
encoding uint32 = jsonEncodingType
// use uint32 for atomic operations
disableLog 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
writer = new(atomicWriter)
setupOnce sync.Once
)
type (
logEntry struct {
Timestamp string `json:"@timestamp"`
Level string `json:"level"`
Duration string `json:"duration,omitempty"`
Content interface{} `json:"content"`
}
logOptions struct {
gzipEnabled bool
logStackCooldownMills int
keepDays int
// 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 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
logEntry map[string]interface{}
logOptions struct {
gzipEnabled bool
logStackCooldownMills int
keepDays int
maxBackups int
maxSize int
rotationRule string
}
)
// 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.
func Alert(v string) {
outputText(errorLog, levelAlert, v)
getWriter().Alert(v)
}
// Close closes the logging.
func Close() error {
if writeConsole {
return nil
}
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
}
if w := writer.Swap(nil); w != nil {
return w.(io.Closer).Close()
}
return nil
}
// Debug writes v into access log.
func Debug(v ...interface{}) {
writeDebug(fmt.Sprint(v...))
}
// Debugf writes v with format into access log.
func Debugf(format string, v ...interface{}) {
writeDebug(fmt.Sprintf(format, v...))
}
// Debugv writes v into access log with json content.
func Debugv(v interface{}) {
writeDebug(v)
}
// Debugw writes msg along with fields into access log.
func Debugw(msg string, fields ...LogField) {
writeDebug(msg, fields...)
}
// Disable disables the logging.
func Disable() {
once.Do(func() {
atomic.StoreUint32(&initialized, 1)
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
})
atomic.StoreUint32(&disableLog, 1)
writer.Store(nopWriter{})
}
// DisableStat disables the stat logs.
@@ -224,65 +97,115 @@ func DisableStat() {
// Error writes v into error log.
func Error(v ...interface{}) {
ErrorCaller(1, 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)
writeError(fmt.Sprint(v...))
}
// Errorf writes v with format into error log.
func Errorf(format string, v ...interface{}) {
ErrorCallerf(1, format, v...)
writeError(fmt.Errorf(format, v...).Error())
}
// ErrorStack writes v along with call stack into error log.
func ErrorStack(v ...interface{}) {
// there is newline in stack string
stackSync(fmt.Sprint(v...))
writeStack(fmt.Sprint(v...))
}
// ErrorStackf writes v along with call stack in format into error log.
func ErrorStackf(format string, v ...interface{}) {
// there is newline in stack string
stackSync(fmt.Sprintf(format, v...))
writeStack(fmt.Sprintf(format, v...))
}
// Errorv writes v into error log with json content.
// No call stack attached, because not elegant to pack the messages.
func Errorv(v interface{}) {
errorAnySync(v)
writeError(v)
}
// Errorw writes msg along with fields into error log.
func Errorw(msg string, fields ...LogField) {
writeError(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.
func Info(v ...interface{}) {
infoTextSync(fmt.Sprint(v...))
writeInfo(fmt.Sprint(v...))
}
// Infof writes v with format into access log.
func Infof(format string, v ...interface{}) {
infoTextSync(fmt.Sprintf(format, v...))
writeInfo(fmt.Sprintf(format, v...))
}
// Infov writes v into access log with json content.
func Infov(v interface{}) {
infoAnySync(v)
writeInfo(v)
}
// Infow writes msg along with fields into access log.
func Infow(msg string, fields ...LogField) {
writeInfo(msg, fields...)
}
// Must checks if err is nil, otherwise logs the error and exits.
func Must(err error) {
if err != nil {
msg := formatWithCaller(err.Error(), 3)
log.Print(msg)
outputText(severeLog, levelFatal, msg)
os.Exit(1)
if err == nil {
return
}
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.
@@ -290,39 +213,85 @@ func SetLevel(level uint32) {
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.
func Severe(v ...interface{}) {
severeSync(fmt.Sprint(v...))
writeSevere(fmt.Sprint(v...))
}
// Severef writes v with format into severe log.
func Severef(format string, v ...interface{}) {
severeSync(fmt.Sprintf(format, v...))
writeSevere(fmt.Sprintf(format, v...))
}
// Slow writes v into slow log.
func Slow(v ...interface{}) {
slowTextSync(fmt.Sprint(v...))
writeSlow(fmt.Sprint(v...))
}
// Slowf writes v with format into slow log.
func Slowf(format string, v ...interface{}) {
slowTextSync(fmt.Sprintf(format, v...))
writeSlow(fmt.Sprintf(format, v...))
}
// Slowv writes v into slow log with json content.
func Slowv(v interface{}) {
slowAnySync(v)
writeSlow(v)
}
// Sloww writes msg along with fields into slow log.
func Sloww(msg string, fields ...LogField) {
writeSlow(msg, fields...)
}
// Stat writes v into stat log.
func Stat(v ...interface{}) {
statSync(fmt.Sprint(v...))
writeStat(fmt.Sprint(v...))
}
// Statf writes v with format into stat log.
func Statf(format string, v ...interface{}) {
statSync(fmt.Sprintf(format, v...))
writeStat(fmt.Sprintf(format, v...))
}
// WithCooldownMillis customizes logging on writing call stack interval.
@@ -346,63 +315,53 @@ 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 addCaller(fields ...LogField) []LogField {
return append(fields, Field(callerKey, getCaller(callerDepth)))
}
func createOutput(path string) (io.WriteCloser, error) {
if len(path) == 0 {
return nil, ErrLogPathNotSet
}
return NewLogger(path, DefaultRotateRule(path, backupFileDelimiter, options.keepDays,
options.gzipEnabled), options.gzipEnabled)
}
func errorAnySync(v interface{}) {
if shallLog(ErrorLevel) {
outputAny(errorLog, levelError, v)
switch options.rotationRule {
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 errorTextSync(msg string, callDepth int) {
if shallLog(ErrorLevel) {
outputError(errorLog, msg, callDepth)
}
}
func formatWithCaller(msg string, callDepth int) string {
var buf strings.Builder
caller := getCaller(callDepth)
if len(caller) > 0 {
buf.WriteString(caller)
buf.WriteByte(' ')
func getWriter() Writer {
w := writer.Load()
if w == nil {
w = writer.StoreIfNil(newConsoleWriter())
}
buf.WriteString(msg)
return buf.String()
}
func getCaller(callDepth int) string {
var buf strings.Builder
_, file, line, ok := runtime.Caller(callDepth)
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()
}
func getTimestamp() string {
return timex.Time().Format(timeFormat)
return w
}
func handleOptions(opts []LogOption) {
@@ -411,63 +370,10 @@ func handleOptions(opts []LogOption) {
}
}
func infoAnySync(val interface{}) {
if shallLog(InfoLevel) {
outputAny(infoLog, levelInfo, val)
}
}
func infoTextSync(msg string) {
if shallLog(InfoLevel) {
outputText(infoLog, levelInfo, 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'))
}
}
func setupLogLevel(c LogConf) {
switch c.Level {
case levelDebug:
SetLevel(DebugLevel)
case levelInfo:
SetLevel(InfoLevel)
case levelError:
@@ -477,72 +383,18 @@ func setupLogLevel(c LogConf) {
}
}
func setupWithConsole(c LogConf) {
once.Do(func() {
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 setupWithConsole() {
SetWriter(newConsoleWriter())
}
func setupWithFiles(c LogConf) error {
var opts []LogOption
var err error
if len(c.Path) == 0 {
return ErrLogPathNotSet
w, err := newFileWriter(c)
if err != nil {
return err
}
opts = append(opts, WithCooldownMillis(c.StackCooldownMillis))
if c.Compress {
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
SetWriter(w)
return nil
}
func setupWithVolume(c LogConf) error {
@@ -554,12 +406,6 @@ func setupWithVolume(c LogConf) error {
return setupWithFiles(c)
}
func severeSync(msg string) {
if shallLog(SevereLevel) {
outputText(severeLog, levelSevere, fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
}
func shallLog(level uint32) bool {
return atomic.LoadUint32(&logLevel) <= level
}
@@ -568,101 +414,44 @@ func shallLogStat() bool {
return atomic.LoadUint32(&disableStat) == 0
}
func slowAnySync(v interface{}) {
if shallLog(ErrorLevel) {
outputAny(slowLog, levelSlow, v)
func writeDebug(val interface{}, fields ...LogField) {
if shallLog(DebugLevel) {
getWriter().Debug(val, addCaller(fields...)...)
}
}
func slowTextSync(msg string) {
func writeError(val interface{}, fields ...LogField) {
if shallLog(ErrorLevel) {
outputText(slowLog, levelSlow, msg)
getWriter().Error(val, addCaller(fields...)...)
}
}
func stackSync(msg string) {
if shallLog(ErrorLevel) {
outputText(stackLog, levelError, fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
func writeInfo(val interface{}, fields ...LogField) {
if shallLog(InfoLevel) {
getWriter().Info(val, addCaller(fields...)...)
}
}
func statSync(msg string) {
func writeSevere(msg string) {
if shallLog(SevereLevel) {
getWriter().Severe(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
}
func writeSlow(val interface{}, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Slow(val, addCaller(fields...)...)
}
}
func writeStack(msg string) {
if shallLog(ErrorLevel) {
getWriter().Stack(fmt.Sprintf("%s\n%s", msg, string(debug.Stack())))
}
}
func writeStat(msg string) {
if shallLogStat() && shallLog(InfoLevel) {
outputText(statLog, levelStat, msg)
getWriter().Stat(msg, addCaller()...)
}
}
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"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"reflect"
"runtime"
"strings"
"sync"
@@ -19,8 +19,9 @@ import (
)
var (
s = []byte("Sending #11 notification (id: 1451875113812010473) in #1 connection")
pool = make(chan []byte, 1)
s = []byte("Sending #11 notification (id: 1451875113812010473) in #1 connection")
pool = make(chan []byte, 1)
_ Writer = (*mockWriter)(nil)
)
type mockWriter struct {
@@ -28,10 +29,52 @@ type mockWriter struct {
builder strings.Builder
}
func (mw *mockWriter) Write(data []byte) (int, error) {
func (mw *mockWriter) Alert(v interface{}) {
mw.lock.Lock()
defer mw.lock.Unlock()
return mw.builder.Write(data)
output(&mw.builder, levelAlert, v)
}
func (mw *mockWriter) Debug(v interface{}, fields ...LogField) {
mw.lock.Lock()
defer mw.lock.Unlock()
output(&mw.builder, levelDebug, v, fields...)
}
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 {
@@ -56,95 +99,251 @@ func (mw *mockWriter) String() 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) {
writer := new(mockWriter)
errorLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
file, line := getFileLine()
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()
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) {
writer := new(mockWriter)
writeConsole = true
errorLog = newLogWriter(log.New(writer, "[ERROR] ", flags))
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
file, line := getFileLine()
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()
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) {
doTestStructedLog(t, levelAlert, func(writer io.WriteCloser) {
errorLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelAlert, w, func(v ...interface{}) {
Alert(fmt.Sprint(v...))
})
}
func TestStructedLogDebug(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
Debug(v...)
})
}
func TestStructedLogDebugf(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
Debugf(fmt.Sprint(v...))
})
}
func TestStructedLogDebugv(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
Debugv(fmt.Sprint(v...))
})
}
func TestStructedLogDebugw(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelDebug, w, func(v ...interface{}) {
Debugw(fmt.Sprint(v...), Field("foo", time.Second))
})
}
func TestStructedLogError(t *testing.T) {
doTestStructedLog(t, levelError, func(writer io.WriteCloser) {
errorLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
Error(v...)
})
}
func TestStructedLogErrorf(t *testing.T) {
doTestStructedLog(t, levelError, func(writer io.WriteCloser) {
errorLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
Errorf("%s", fmt.Sprint(v...))
})
}
func TestStructedLogErrorv(t *testing.T) {
doTestStructedLog(t, levelError, func(writer io.WriteCloser) {
errorLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelError, w, func(v ...interface{}) {
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) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
Info(v...)
})
}
func TestStructedLogInfof(t *testing.T) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
Infof("%s", fmt.Sprint(v...))
})
}
func TestStructedLogInfov(t *testing.T) {
doTestStructedLog(t, levelInfo, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelInfo, w, func(v ...interface{}) {
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) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType)
defer func() {
@@ -156,9 +355,11 @@ func TestStructedLogInfoConsoleAny(t *testing.T) {
}
func TestStructedLogInfoConsoleAnyString(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType)
defer func() {
@@ -170,9 +371,11 @@ func TestStructedLogInfoConsoleAnyString(t *testing.T) {
}
func TestStructedLogInfoConsoleAnyError(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType)
defer func() {
@@ -184,9 +387,11 @@ func TestStructedLogInfoConsoleAnyError(t *testing.T) {
}
func TestStructedLogInfoConsoleAnyStringer(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType)
defer func() {
@@ -200,9 +405,11 @@ func TestStructedLogInfoConsoleAnyStringer(t *testing.T) {
}
func TestStructedLogInfoConsoleText(t *testing.T) {
doTestStructedLogConsole(t, func(writer io.WriteCloser) {
infoLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLogConsole(t, w, func(v ...interface{}) {
old := atomic.LoadUint32(&encoding)
atomic.StoreUint32(&encoding, plainEncodingType)
defer func() {
@@ -214,84 +421,110 @@ func TestStructedLogInfoConsoleText(t *testing.T) {
}
func TestStructedLogSlow(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) {
slowLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
Slow(v...)
})
}
func TestStructedLogSlowf(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) {
slowLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
Slowf(fmt.Sprint(v...))
})
}
func TestStructedLogSlowv(t *testing.T) {
doTestStructedLog(t, levelSlow, func(writer io.WriteCloser) {
slowLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSlow, w, func(v ...interface{}) {
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) {
doTestStructedLog(t, levelStat, func(writer io.WriteCloser) {
statLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelStat, w, func(v ...interface{}) {
Stat(v...)
})
}
func TestStructedLogStatf(t *testing.T) {
doTestStructedLog(t, levelStat, func(writer io.WriteCloser) {
statLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelStat, w, func(v ...interface{}) {
Statf(fmt.Sprint(v...))
})
}
func TestStructedLogSevere(t *testing.T) {
doTestStructedLog(t, levelSevere, func(writer io.WriteCloser) {
severeLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSevere, w, func(v ...interface{}) {
Severe(v...)
})
}
func TestStructedLogSeveref(t *testing.T) {
doTestStructedLog(t, levelSevere, func(writer io.WriteCloser) {
severeLog = writer
}, func(v ...interface{}) {
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
doTestStructedLog(t, levelSevere, w, func(v ...interface{}) {
Severef(fmt.Sprint(v...))
})
}
func TestStructedLogWithDuration(t *testing.T) {
const message = "hello there"
writer := new(mockWriter)
infoLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Info(message)
var entry logEntry
if err := json.Unmarshal([]byte(writer.builder.String()), &entry); err != nil {
var entry map[string]interface{}
if err := json.Unmarshal([]byte(w.String()), &entry); err != nil {
t.Error(err)
}
assert.Equal(t, levelInfo, entry.Level)
assert.Equal(t, message, entry.Content)
assert.Equal(t, "1000.0ms", entry.Duration)
assert.Equal(t, levelInfo, entry[levelKey])
assert.Equal(t, message, entry[contentKey])
assert.Equal(t, "1000.0ms", entry[durationKey])
}
func TestSetLevel(t *testing.T) {
SetLevel(ErrorLevel)
const message = "hello there"
writer := new(mockWriter)
infoLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
Info(message)
assert.Equal(t, 0, writer.builder.Len())
assert.Equal(t, 0, w.builder.Len())
}
func TestSetLevelTwiceWithMode(t *testing.T) {
@@ -300,29 +533,35 @@ func TestSetLevelTwiceWithMode(t *testing.T) {
"console",
"volumn",
}
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
for _, mode := range testModes {
testSetLevelTwiceWithMode(t, mode)
testSetLevelTwiceWithMode(t, mode, w)
}
}
func TestSetLevelWithDuration(t *testing.T) {
SetLevel(ErrorLevel)
const message = "hello there"
writer := new(mockWriter)
infoLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
WithDuration(time.Second).Info(message)
assert.Equal(t, 0, writer.builder.Len())
assert.Equal(t, 0, w.builder.Len())
}
func TestErrorfWithWrappedError(t *testing.T) {
SetLevel(ErrorLevel)
const message = "there"
writer := new(mockWriter)
errorLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
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) {
@@ -330,9 +569,15 @@ func TestMustNil(t *testing.T) {
}
func TestSetup(t *testing.T) {
defer func() {
SetLevel(InfoLevel)
atomic.StoreUint32(&encoding, jsonEncodingType)
}()
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
TimeFormat: timeFormat,
})
MustSetup(LogConf{
ServiceName: "any",
@@ -344,13 +589,34 @@ func TestSetup(t *testing.T) {
Mode: "volume",
Path: os.TempDir(),
})
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
TimeFormat: timeFormat,
})
MustSetup(LogConf{
ServiceName: "any",
Mode: "console",
Encoding: plainEncoding,
})
defer os.RemoveAll("CD01CB7D-2705-4F3F-889E-86219BF56F10")
assert.NotNil(t, setupWithVolume(LogConf{}))
assert.Nil(t, setupWithVolume(LogConf{
ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10",
}))
assert.Nil(t, setupWithVolume(LogConf{
ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10",
Rotation: sizeRotationRule,
}))
assert.NotNil(t, setupWithFiles(LogConf{}))
assert.Nil(t, setupWithFiles(LogConf{
ServiceName: "any",
Path: os.TempDir(),
Compress: true,
KeepDays: 1,
MaxBackups: 3,
MaxSize: 1024 * 1024,
}))
setupLogLevel(LogConf{
Level: levelInfo,
@@ -364,6 +630,8 @@ func TestSetup(t *testing.T) {
_, err := createOutput("")
assert.NotNil(t, err)
Disable()
SetLevel(InfoLevel)
atomic.StoreUint32(&encoding, jsonEncodingType)
}
func TestDisable(t *testing.T) {
@@ -372,8 +640,9 @@ func TestDisable(t *testing.T) {
var opt logOptions
WithKeepDays(1)(&opt)
WithGzip()(&opt)
WithMaxBackups(1)(&opt)
WithMaxSize(1024)(&opt)
assert.Nil(t, Close())
writeConsole = false
assert.Nil(t, Close())
}
@@ -381,11 +650,22 @@ func TestDisableStat(t *testing.T) {
DisableStat()
const message = "hello there"
writer := new(mockWriter)
statLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
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) {
@@ -428,7 +708,7 @@ func BenchmarkCopyByteSlice(b *testing.B) {
buf = make([]byte, len(s))
copy(buf, s)
}
fmt.Fprint(ioutil.Discard, buf)
fmt.Fprint(io.Discard, buf)
}
func BenchmarkCopyOnWriteByteSlice(b *testing.B) {
@@ -437,7 +717,7 @@ func BenchmarkCopyOnWriteByteSlice(b *testing.B) {
size := len(s)
buf = s[:size:size]
}
fmt.Fprint(ioutil.Discard, buf)
fmt.Fprint(io.Discard, buf)
}
func BenchmarkCacheByteSlice(b *testing.B) {
@@ -451,7 +731,7 @@ func BenchmarkCacheByteSlice(b *testing.B) {
func BenchmarkLogs(b *testing.B) {
b.ReportAllocs()
log.SetOutput(ioutil.Discard)
log.SetOutput(io.Discard)
for i := 0; i < b.N; i++ {
Info(i)
}
@@ -487,35 +767,29 @@ func put(b []byte) {
}
}
func doTestStructedLog(t *testing.T, level string, setup func(writer io.WriteCloser),
write func(...interface{})) {
func doTestStructedLog(t *testing.T, level string, w *mockWriter, write func(...interface{})) {
const message = "hello there"
writer := new(mockWriter)
setup(writer)
atomic.StoreUint32(&initialized, 1)
write(message)
var entry logEntry
if err := json.Unmarshal([]byte(writer.builder.String()), &entry); err != nil {
var entry map[string]interface{}
if err := json.Unmarshal([]byte(w.String()), &entry); err != nil {
t.Error(err)
}
assert.Equal(t, level, entry.Level)
val, ok := entry.Content.(string)
assert.Equal(t, level, entry[levelKey])
val, ok := entry[contentKey]
assert.True(t, ok)
assert.True(t, strings.Contains(val, message))
assert.True(t, strings.Contains(val.(string), message))
}
func doTestStructedLogConsole(t *testing.T, setup func(writer io.WriteCloser),
write func(...interface{})) {
func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...interface{})) {
const message = "hello there"
writer := new(mockWriter)
setup(writer)
atomic.StoreUint32(&initialized, 1)
write(message)
println(writer.String())
assert.True(t, strings.Contains(writer.String(), message))
assert.True(t, strings.Contains(w.String(), message))
}
func testSetLevelTwiceWithMode(t *testing.T, mode string) {
func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) {
writer.Store(nil)
SetUp(LogConf{
Mode: mode,
Level: "error",
@@ -527,17 +801,14 @@ func testSetLevelTwiceWithMode(t *testing.T, mode string) {
Path: "/dev/null",
})
const message = "hello there"
writer := new(mockWriter)
infoLog = writer
atomic.StoreUint32(&initialized, 1)
Info(message)
assert.Equal(t, 0, writer.builder.Len())
assert.Equal(t, 0, w.builder.Len())
Infof(message)
assert.Equal(t, 0, writer.builder.Len())
assert.Equal(t, 0, w.builder.Len())
ErrorStack(message)
assert.Equal(t, 0, writer.builder.Len())
assert.Equal(t, 0, w.builder.Len())
ErrorStackf(message)
assert.Equal(t, 0, writer.builder.Len())
assert.Equal(t, 0, w.builder.Len())
}
type ValStringer struct {
@@ -547,3 +818,18 @@ type ValStringer struct {
func (v ValStringer) String() string {
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!

199
core/logx/richlogger.go Normal file
View File

@@ -0,0 +1,199 @@
package logx
import (
"context"
"fmt"
"time"
"github.com/zeromicro/go-zero/core/timex"
"go.opentelemetry.io/otel/trace"
)
// WithCallerSkip returns a Logger with given caller skip.
func WithCallerSkip(skip int) Logger {
if skip <= 0 {
return new(richLogger)
}
return &richLogger{
callerSkip: skip,
}
}
// WithContext sets ctx to log, for keeping tracing information.
func WithContext(ctx context.Context) Logger {
return &richLogger{
ctx: ctx,
}
}
// WithDuration returns a Logger with given duration.
func WithDuration(d time.Duration) Logger {
return &richLogger{
fields: []LogField{Field(durationKey, timex.ReprOfDuration(d))},
}
}
type richLogger struct {
ctx context.Context
callerSkip int
fields []LogField
}
func (l *richLogger) Debug(v ...interface{}) {
l.debug(fmt.Sprint(v...))
}
func (l *richLogger) Debugf(format string, v ...interface{}) {
l.debug(fmt.Sprintf(format, v...))
}
func (l *richLogger) Debugv(v interface{}) {
l.debug(v)
}
func (l *richLogger) Debugw(msg string, fields ...LogField) {
l.debug(msg, fields...)
}
func (l *richLogger) Error(v ...interface{}) {
l.err(fmt.Sprint(v...))
}
func (l *richLogger) Errorf(format string, v ...interface{}) {
l.err(fmt.Sprintf(format, v...))
}
func (l *richLogger) Errorv(v interface{}) {
l.err(fmt.Sprint(v))
}
func (l *richLogger) Errorw(msg string, fields ...LogField) {
l.err(msg, fields...)
}
func (l *richLogger) Info(v ...interface{}) {
l.info(fmt.Sprint(v...))
}
func (l *richLogger) Infof(format string, v ...interface{}) {
l.info(fmt.Sprintf(format, v...))
}
func (l *richLogger) Infov(v interface{}) {
l.info(v)
}
func (l *richLogger) Infow(msg string, fields ...LogField) {
l.info(msg, fields...)
}
func (l *richLogger) Slow(v ...interface{}) {
l.slow(fmt.Sprint(v...))
}
func (l *richLogger) Slowf(format string, v ...interface{}) {
l.slow(fmt.Sprintf(format, v...))
}
func (l *richLogger) Slowv(v interface{}) {
l.slow(v)
}
func (l *richLogger) Sloww(msg string, fields ...LogField) {
l.slow(msg, fields...)
}
func (l *richLogger) WithCallerSkip(skip int) Logger {
if skip <= 0 {
return l
}
l.callerSkip = skip
return l
}
func (l *richLogger) WithContext(ctx context.Context) Logger {
l.ctx = ctx
return l
}
func (l *richLogger) WithDuration(duration time.Duration) Logger {
l.fields = append(l.fields, Field(durationKey, timex.ReprOfDuration(duration)))
return l
}
func (l *richLogger) WithFields(fields ...LogField) Logger {
l.fields = append(l.fields, fields...)
return l
}
func (l *richLogger) buildFields(fields ...LogField) []LogField {
fields = append(l.fields, fields...)
fields = append(fields, Field(callerKey, getCaller(callerDepth+l.callerSkip)))
if l.ctx == nil {
return fields
}
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 *richLogger) debug(v interface{}, fields ...LogField) {
if shallLog(DebugLevel) {
getWriter().Debug(v, l.buildFields(fields...)...)
}
}
func (l *richLogger) err(v interface{}, fields ...LogField) {
if shallLog(ErrorLevel) {
getWriter().Error(v, l.buildFields(fields...)...)
}
}
func (l *richLogger) info(v interface{}, fields ...LogField) {
if shallLog(InfoLevel) {
getWriter().Info(v, l.buildFields(fields...)...)
}
}
func (l *richLogger) 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,318 @@
package logx
import (
"context"
"encoding/json"
"fmt"
"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 TestTraceDebug(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("foo").Start(context.Background(), "bar")
defer span.End()
l := WithContext(ctx)
SetLevel(DebugLevel)
l.WithDuration(time.Second).Debug(testlog)
assert.True(t, strings.Contains(w.String(), traceKey))
assert.True(t, strings.Contains(w.String(), spanKey))
w.Reset()
l.WithDuration(time.Second).Debugf(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Debugv(testlog)
validate(t, w.String(), true, true)
w.Reset()
l.WithDuration(time.Second).Debugw(testlog, Field("foo", "bar"))
validate(t, w.String(), true, true)
assert.True(t, strings.Contains(w.String(), "foo"), w.String())
assert.True(t, strings.Contains(w.String(), "bar"), w.String())
}
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 := ContextWithFields(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 TestLogWithCallerSkip(t *testing.T) {
w := new(mockWriter)
old := writer.Swap(w)
writer.lock.RLock()
defer func() {
writer.lock.RUnlock()
writer.Store(old)
}()
l := WithCallerSkip(1).WithCallerSkip(0)
p := func(v string) {
l.Info(v)
}
file, line := getFileLine()
p(testlog)
assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
w.Reset()
l = WithCallerSkip(0).WithCallerSkip(1)
file, line = getFileLine()
p(testlog)
assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1)))
}
func TestLoggerWithFields(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()).WithFields(Field("foo", "bar"))
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

@@ -9,21 +9,24 @@ import (
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/zeromicro/go-zero/core/fs"
"github.com/zeromicro/go-zero/core/lang"
"github.com/zeromicro/go-zero/core/timex"
)
const (
dateFormat = "2006-01-02"
fileTimeFormat = time.RFC3339
hoursPerDay = 24
bufferSize = 100
defaultDirMode = 0o755
defaultFileMode = 0o600
gzipExt = ".gz"
megaBytes = 1 << 20
)
// ErrLogFileClosed is an error that indicates the log file is already closed.
@@ -35,7 +38,7 @@ type (
BackupFileName() string
MarkRotated()
OutdatedFiles() []string
ShallRotate() bool
ShallRotate(size int64) bool
}
// A RotateLogger is a Logger that can rotate log files with given rules.
@@ -48,8 +51,9 @@ type (
rule RotateRule
compress bool
// can't use threading.RoutineGroup because of cycle import
waitGroup sync.WaitGroup
closeOnce sync.Once
waitGroup sync.WaitGroup
closeOnce sync.Once
currentSize int64
}
// A DailyRotateRule is a rule to daily rotate the log files.
@@ -60,6 +64,13 @@ type (
days int
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.
@@ -91,7 +102,7 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
var pattern string
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 {
pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter)
}
@@ -104,9 +115,11 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
var buf strings.Builder
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat)
fmt.Fprintf(&buf, "%s%s%s", r.filename, r.delimiter, boundary)
buf.WriteString(r.filename)
buf.WriteString(r.delimiter)
buf.WriteString(boundary)
if r.gzip {
buf.WriteString(".gz")
buf.WriteString(gzipExt)
}
boundaryFile := buf.String()
@@ -121,10 +134,100 @@ func (r *DailyRotateRule) OutdatedFiles() []string {
}
// 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
}
// 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.
func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) {
l := &RotateLogger{
@@ -211,6 +314,12 @@ func (l *RotateLogger) maybeCompressFile(file string) {
ErrorStack(r)
}
}()
if _, err := os.Stat(file); err != nil {
// file not exists or other error, ignore compression
return
}
compressLogFile(file)
}
@@ -277,25 +386,27 @@ func (l *RotateLogger) startWorker() {
}
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 {
log.Println(err)
} else {
l.rule.MarkRotated()
l.currentSize = 0
}
}
if l.fp != nil {
l.fp.Write(v)
l.currentSize += int64(len(v))
}
}
func compressLogFile(file string) {
start := timex.Now()
start := time.Now()
Infof("compressing log file: %s", file)
if err := gzipFile(file); err != nil {
Errorf("compress error: %s", err)
} 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 +414,10 @@ func getNowDate() string {
return time.Now().Format(dateFormat)
}
func getNowDateInRFC3339Format() string {
return time.Now().Format(fileTimeFormat)
}
func gzipFile(file string) error {
in, err := os.Open(file)
if err != nil {
@@ -310,7 +425,7 @@ func gzipFile(file string) error {
}
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 {
return err
}

View File

@@ -29,7 +29,34 @@ func TestDailyRotateRuleOutdatedFiles(t *testing.T) {
func TestDailyRotateRuleShallRotate(t *testing.T) {
var rule DailyRotateRule
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) {
@@ -57,6 +84,12 @@ func TestRotateLoggerGetBackupFilename(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")
assert.Nil(t, err)
if len(filename) > 0 {
@@ -70,15 +103,18 @@ func TestRotateLoggerMayCompressFile(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")
assert.Nil(t, err)
logger, err := NewLogger(filename, new(DailyRotateRule), true)
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(filename)
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
defer os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}
logger.maybeCompressFile(filename)
_, err = os.Stat(filename)
@@ -92,7 +128,6 @@ func TestRotateLoggerRotate(t *testing.T) {
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(filename)
os.Remove(logger.getBackupFilename())
os.Remove(filepath.Base(logger.getBackupFilename()) + ".gz")
}()
@@ -102,6 +137,10 @@ func TestRotateLoggerRotate(t *testing.T) {
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)
}
@@ -115,12 +154,177 @@ func TestRotateLoggerWrite(t *testing.T) {
assert.Nil(t, err)
if len(filename) > 0 {
defer func() {
os.Remove(filename)
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 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,24 +29,31 @@ func TestRedirector(t *testing.T) {
}
func captureOutput(f func()) string {
writer := new(mockWriter)
infoLog = writer
atomic.StoreUint32(&initialized, 1)
w := new(mockWriter)
old := writer.Swap(w)
defer writer.Store(old)
prevLevel := atomic.LoadUint32(&logLevel)
SetLevel(InfoLevel)
f()
SetLevel(prevLevel)
return writer.builder.String()
return w.String()
}
func getContent(jsonStr string) string {
var entry logEntry
var entry map[string]interface{}
json.Unmarshal([]byte(jsonStr), &entry)
val, ok := entry.Content.(string)
if ok {
return val
val, ok := entry[contentKey]
if !ok {
return ""
}
return ""
str, ok := val.(string)
if !ok {
return ""
}
return str
}

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)
}
}

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

@@ -0,0 +1,63 @@
package logx
import "errors"
const (
// DebugLevel logs everything
DebugLevel uint32 = iota
// InfoLevel does not include debugs
InfoLevel
// 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"
levelDebug = "debug"
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")
)

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

@@ -0,0 +1,390 @@
package logx
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"path"
"sync"
"sync/atomic"
fatihcolor "github.com/fatih/color"
"github.com/zeromicro/go-zero/core/color"
)
type (
Writer interface {
Alert(v interface{})
Close() error
Debug(v interface{}, fields ...LogField)
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) Debug(v interface{}, fields ...LogField) {
output(w.infoLog, levelDebug, v, fields...)
}
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) Debug(_ interface{}, _ ...LogField) {
}
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 combineGlobalFields(fields []LogField) []LogField {
globals := globalFields.Load()
if globals == nil {
return fields
}
return append(globals.([]LogField), fields...)
}
func output(writer io.Writer, level string, val interface{}, fields ...LogField) {
fields = combineGlobalFields(fields)
switch atomic.LoadUint32(&encoding) {
case plainEncodingType:
writePlainAny(writer, level, val, buildFields(fields...)...)
default:
entry := make(logEntry)
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 levelDebug:
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 bytes.Buffer
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 := writer.Write(buf.Bytes()); err != nil {
log.Println(err.Error())
}
}
func writePlainValue(writer io.Writer, level string, val interface{}, fields ...string) {
var buf bytes.Buffer
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 := writer.Write(buf.Bytes()); err != nil {
log.Println(err.Error())
}
}

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

@@ -0,0 +1,189 @@
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)
buf.Reset()
w.Debug(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.Debug("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, levelDebug, make(chan int))
assert.Contains(t, buf.String(), "unsupported type")
writePlainAny(nil, levelDebug, 100)
assert.Contains(t, buf.String(), "100")
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")
}

View File

@@ -261,6 +261,78 @@ func TestMarshal_RangeOut(t *testing.T) {
}
}
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"`

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

@@ -1,6 +1,7 @@
package mapping
import (
"encoding"
"encoding/json"
"errors"
"fmt"
@@ -25,9 +26,9 @@ var (
errValueNotStruct = errors.New("value type is not struct")
keyUnmarshaler = NewUnmarshaler(defaultKeyName)
durationType = reflect.TypeOf(time.Duration(0))
cacheKeys map[string][]string
cacheKeys = make(map[string][]string)
cacheKeysLock sync.Mutex
defaultCache map[string]interface{}
defaultCache = make(map[string]interface{})
defaultCacheLock sync.Mutex
emptyMap = map[string]interface{}{}
emptyValue = reflect.ValueOf(lang.Placeholder)
@@ -49,11 +50,6 @@ type (
}
)
func init() {
cacheKeys = make(map[string][]string)
defaultCache = make(map[string]interface{})
}
// NewUnmarshaler returns a Unmarshaler.
func NewUnmarshaler(key string, opts ...UnmarshalOption) *Unmarshaler {
unmarshaler := Unmarshaler{
@@ -144,7 +140,6 @@ func (u *Unmarshaler) processAnonymousFieldOptional(field reflect.StructField, v
filled = true
maybeNewValue(field, value)
indirectValue = reflect.Indirect(value)
}
if err = u.processField(subField, indirectValue.Field(i), m, fullName); err != nil {
return err
@@ -205,6 +200,8 @@ func (u *Unmarshaler) processFieldNotFromString(field reflect.StructField, value
return u.processFieldStruct(field, value, mapValue, fullName)
case valueKind == reflect.Map && typeKind == reflect.Map:
return u.fillMap(field, value, mapValue)
case valueKind == reflect.String && typeKind == reflect.Map:
return u.fillMapFromString(value, mapValue)
case valueKind == reflect.String && typeKind == reflect.Slice:
return u.fillSliceFromString(fieldType, value, mapValue)
case valueKind == reflect.String && derefedFieldType == durationType:
@@ -231,7 +228,7 @@ func (u *Unmarshaler) processFieldPrimitive(field reflect.StructField, value ref
return u.processFieldPrimitiveWithJSONNumber(field, value, v, opts, fullName)
default:
if typeKind == valueKind {
if err := validateValueInOptions(opts.options(), mapValue); err != nil {
if err := validateValueInOptions(mapValue, opts.options()); err != nil {
return err
}
@@ -253,6 +250,10 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(field reflect.StructFi
return err
}
if err := validateValueInOptions(v, opts.options()); err != nil {
return err
}
if fieldKind == reflect.Ptr {
value = value.Elem()
}
@@ -318,6 +319,28 @@ func (u *Unmarshaler) processFieldStructWithMap(field reflect.StructField, value
return nil
}
func (u *Unmarshaler) processFieldTextUnmarshaler(field reflect.StructField, value reflect.Value,
mapValue interface{}) (bool, error) {
var tval encoding.TextUnmarshaler
var ok bool
if field.Type.Kind() == reflect.Ptr {
tval, ok = value.Interface().(encoding.TextUnmarshaler)
} else {
tval, ok = value.Addr().Interface().(encoding.TextUnmarshaler)
}
if ok {
switch mv := mapValue.(type) {
case string:
return true, tval.UnmarshalText([]byte(mv))
case []byte:
return true, tval.UnmarshalText(mv)
}
}
return false, nil
}
func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect.Value,
m Valuer, fullName string) error {
key, opts, err := u.parseOptionsWithContext(field, m, fullName)
@@ -348,8 +371,16 @@ func (u *Unmarshaler) processNamedFieldWithValue(field reflect.StructField, valu
return fmt.Errorf("field %s mustn't be nil", key)
}
if !value.CanSet() {
return fmt.Errorf("field %s is not settable", key)
}
maybeNewValue(field, value)
if yes, err := u.processFieldTextUnmarshaler(field, value, mapValue); yes {
return err
}
fieldKind := Deref(field.Type).Kind()
switch fieldKind {
case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
@@ -438,6 +469,27 @@ func (u *Unmarshaler) fillMap(field reflect.StructField, value reflect.Value, ma
return nil
}
func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue interface{}) error {
if !value.CanSet() {
return errValueNotSettable
}
switch v := mapValue.(type) {
case fmt.Stringer:
if err := jsonx.UnmarshalFromString(v.String(), value.Addr().Interface()); err != nil {
return err
}
case string:
if err := jsonx.UnmarshalFromString(v, value.Addr().Interface()); err != nil {
return err
}
default:
return errUnsupportedType
}
return nil
}
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue interface{}) error {
if !value.CanSet() {
return errValueNotSettable
@@ -496,10 +548,20 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
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{}
if err := jsonx.UnmarshalFromString(mapValue.(string), &slice); err != nil {
return err
switch v := mapValue.(type) {
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())
@@ -520,8 +582,10 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
baseKind reflect.Kind, value interface{}) error {
ithVal := slice.Index(index)
switch v := value.(type) {
case json.Number:
case fmt.Stringer:
return setValue(baseKind, ithVal, v.String())
case string:
return setValue(baseKind, ithVal, v)
default:
// don't need to consider the difference between int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.

View File

@@ -2,11 +2,13 @@ package mapping
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stringx"
)
@@ -467,6 +469,146 @@ func TestUnmarshalIntSliceFromString(t *testing.T) {
ast.Equal(2, v.Values[1])
}
func TestUnmarshalIntMapFromString(t *testing.T) {
var v struct {
Sort map[string]int `key:"sort"`
}
m := map[string]interface{}{
"sort": `{"value":12345,"zeroVal":0,"nullVal":null}`,
}
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &v))
ast.Equal(3, len(v.Sort))
ast.Equal(12345, v.Sort["value"])
ast.Equal(0, v.Sort["zeroVal"])
ast.Equal(0, v.Sort["nullVal"])
}
func TestUnmarshalBoolMapFromString(t *testing.T) {
var v struct {
Sort map[string]bool `key:"sort"`
}
m := map[string]interface{}{
"sort": `{"value":true,"zeroVal":false,"nullVal":null}`,
}
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &v))
ast.Equal(3, len(v.Sort))
ast.Equal(true, v.Sort["value"])
ast.Equal(false, v.Sort["zeroVal"])
ast.Equal(false, v.Sort["nullVal"])
}
type CustomStringer string
type UnsupportedStringer string
func (c CustomStringer) String() string {
return fmt.Sprintf("{%s}", string(c))
}
func TestUnmarshalStringMapFromStringer(t *testing.T) {
var v struct {
Sort map[string]string `key:"sort"`
}
m := map[string]interface{}{
"sort": CustomStringer(`"value":"ascend","emptyStr":""`),
}
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &v))
ast.Equal(2, len(v.Sort))
ast.Equal("ascend", v.Sort["value"])
ast.Equal("", v.Sort["emptyStr"])
}
func TestUnmarshalStringMapFromUnsupportedType(t *testing.T) {
var v struct {
Sort map[string]string `key:"sort"`
}
m := map[string]interface{}{
"sort": UnsupportedStringer(`{"value":"ascend","emptyStr":""}`),
}
ast := assert.New(t)
ast.NotNil(UnmarshalKey(m, &v))
}
func TestUnmarshalStringMapFromNotSettableValue(t *testing.T) {
var v struct {
sort map[string]string `key:"sort"`
psort *map[string]string `key:"sort"`
}
m := map[string]interface{}{
"sort": `{"value":"ascend","emptyStr":""}`,
"psort": `{"value":"ascend","emptyStr":""}`,
}
ast := assert.New(t)
ast.NotNil(UnmarshalKey(m, &v))
}
func TestUnmarshalStringMapFromString(t *testing.T) {
var v struct {
Sort map[string]string `key:"sort"`
}
m := map[string]interface{}{
"sort": `{"value":"ascend","emptyStr":""}`,
}
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &v))
ast.Equal(2, len(v.Sort))
ast.Equal("ascend", v.Sort["value"])
ast.Equal("", v.Sort["emptyStr"])
}
func TestUnmarshalStructMapFromString(t *testing.T) {
var v struct {
Filter map[string]struct {
Field1 bool `json:"field1"`
Field2 int64 `json:"field2,string"`
Field3 string `json:"field3"`
Field4 *string `json:"field4"`
Field5 []string `json:"field5"`
} `key:"filter"`
}
m := map[string]interface{}{
"filter": `{"obj":{"field1":true,"field2":"1573570455447539712","field3":"this is a string",
"field4":"this is a string pointer","field5":["str1","str2"]}}`,
}
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &v))
ast.Equal(1, len(v.Filter))
ast.NotNil(v.Filter["obj"])
ast.Equal(true, v.Filter["obj"].Field1)
ast.Equal(int64(1573570455447539712), v.Filter["obj"].Field2)
ast.Equal("this is a string", v.Filter["obj"].Field3)
ast.Equal("this is a string pointer", *v.Filter["obj"].Field4)
ast.ElementsMatch([]string{"str1", "str2"}, v.Filter["obj"].Field5)
}
func TestUnmarshalStringSliceMapFromString(t *testing.T) {
var v struct {
Filter map[string][]string `key:"filter"`
}
m := map[string]interface{}{
"filter": `{"assignType":null,"status":["process","comment"],"rate":[]}`,
}
ast := assert.New(t)
ast.Nil(UnmarshalKey(m, &v))
ast.Equal(3, len(v.Filter))
ast.Equal([]string(nil), v.Filter["assignType"])
ast.Equal(2, len(v.Filter["status"]))
ast.Equal("process", v.Filter["status"][0])
ast.Equal("comment", v.Filter["status"][1])
ast.Equal(0, len(v.Filter["rate"]))
}
func TestUnmarshalStruct(t *testing.T) {
type address struct {
City string `key:"city"`
@@ -1170,6 +1312,28 @@ func TestUnmarshalWithIntOptionsIncorrect(t *testing.T) {
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) {
type inner struct {
Value string `key:"value,options=first|second"`
@@ -2659,7 +2823,7 @@ func TestUnmarshalJsonReaderMultiArray(t *testing.T) {
assert.Equal(t, 2, len(res.B))
}
func TestUnmarshalJsonReaderPtrMultiArray(t *testing.T) {
func TestUnmarshalJsonReaderPtrMultiArrayString(t *testing.T) {
payload := `{"a": "133", "b": [["add", "cccd"], ["eeee"]]}`
var res struct {
A string `json:"a"`
@@ -2672,6 +2836,32 @@ func TestUnmarshalJsonReaderPtrMultiArray(t *testing.T) {
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) {
payload := `{"a": "133", "b": ["add", "cccd", "eeee"]}`
var res struct {
@@ -2684,6 +2874,30 @@ func TestUnmarshalJsonReaderPtrArray(t *testing.T) {
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) {
payload := `{"A": "1", "B": "2"}`
var res struct {
@@ -2777,6 +2991,54 @@ func TestUnmarshalJsonReaderComplex(t *testing.T) {
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 TestGoogleUUID(t *testing.T) {
var val struct {
Uid uuid.UUID `json:"uid,optional"`
Uidp *uuid.UUID `json:"uidp,optional"`
}
assert.NoError(t, UnmarshalJsonBytes([]byte(`{
"uid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"uidp": "6ba7b810-9dad-11d1-80b4-00c04fd430c9"}`), &val))
assert.Equal(t, "6ba7b810-9dad-11d1-80b4-00c04fd430c8", val.Uid.String())
assert.Equal(t, "6ba7b810-9dad-11d1-80b4-00c04fd430c9", val.Uidp.String())
assert.NoError(t, UnmarshalJsonMap(map[string]interface{}{
"uid": []byte("6ba7b810-9dad-11d1-80b4-00c04fd430c1"),
"uidp": []byte("6ba7b810-9dad-11d1-80b4-00c04fd430c2"),
}, &val))
assert.Equal(t, "6ba7b810-9dad-11d1-80b4-00c04fd430c1", val.Uid.String())
assert.Equal(t, "6ba7b810-9dad-11d1-80b4-00c04fd430c2", val.Uidp.String())
}
func BenchmarkDefaultValue(b *testing.B) {
for i := 0; i < b.N; i++ {
var a struct {

View File

@@ -143,6 +143,23 @@ func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fie
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) {
numFields := tp.NumField()
for i := 0; i < numFields; i++ {
@@ -294,6 +311,20 @@ func parseNumberRange(str string) (*numberRange, error) {
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{
left: left,
leftInclude: leftInclude,
@@ -478,6 +509,7 @@ func setValue(kind reflect.Kind, value reflect.Value, str string) error {
return errValueNotSettable
}
value = ensureValue(value)
v, err := convertType(kind, str)
if err != nil {
return err
@@ -592,16 +624,16 @@ func validateNumberRange(fv float64, nr *numberRange) error {
return nil
}
func validateValueInOptions(options []string, value interface{}) error {
func validateValueInOptions(val interface{}, options []string) error {
if len(options) > 0 {
switch v := value.(type) {
switch v := val.(type) {
case string:
if !stringx.Contains(options, v) {
return fmt.Errorf(`error: value "%s" is not defined in options "%v"`, v, options)
}
default:
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"`
}
func TestDeferInt(t *testing.T) {
func TestDerefInt(t *testing.T) {
i := 1
s := "hello"
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) {
var foo Foo
rte := reflect.TypeOf(&foo).Elem()

View File

@@ -63,7 +63,7 @@ func cleanupMapValue(v interface{}) interface{} {
}
}
func unmarshal(unmarshaler *Unmarshaler, o interface{}, v interface{}) error {
func unmarshal(unmarshaler *Unmarshaler, o, v interface{}) error {
if m, ok := o.(map[string]interface{}); ok {
return unmarshaler.Unmarshal(m, v)
}

View File

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

View File

@@ -5,6 +5,7 @@ import (
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/prometheus"
)
func TestNewCounterVec(t *testing.T) {
@@ -21,6 +22,7 @@ func TestNewCounterVec(t *testing.T) {
}
func TestCounterIncr(t *testing.T) {
startAgent()
counterVec := NewCounterVec(&CounterVecOpts{
Namespace: "http_client",
Subsystem: "call",
@@ -37,6 +39,7 @@ func TestCounterIncr(t *testing.T) {
}
func TestCounterAdd(t *testing.T) {
startAgent()
counterVec := NewCounterVec(&CounterVecOpts{
Namespace: "rpc_server",
Subsystem: "requests",
@@ -51,3 +54,11 @@ func TestCounterAdd(t *testing.T) {
r := testutil.ToFloat64(cv.counter)
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 (
prom "github.com/prometheus/client_golang/prometheus"
"github.com/zeromicro/go-zero/core/proc"
"github.com/zeromicro/go-zero/core/prometheus"
)
type (
@@ -50,14 +51,26 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
}
func (gv *promGaugeVec) Inc(labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Inc()
}
func (gv *promGaugeVec) Add(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Add(v)
}
func (gv *promGaugeVec) Set(v float64, labels ...string) {
if !prometheus.Enabled() {
return
}
gv.gauge.WithLabelValues(labels...).Set(v)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ func FuzzMapReduce(f *testing.F) {
rand.Seed(time.Now().UnixNano())
f.Add(uint(10), uint(runtime.NumCPU()))
f.Fuzz(func(t *testing.T, num uint, workers uint) {
f.Fuzz(func(t *testing.T, num, workers uint) {
n := int64(num)%5000 + 5000
genPanic := rand.Intn(100) == 0
mapperPanic := rand.Intn(100) == 0

View File

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

View File

@@ -1,16 +1,23 @@
package proc
import (
"log"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
)
func TestDumpGoroutines(t *testing.T) {
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()
assert.True(t, strings.Contains(buf.String(), ".dump"))
}

View File

@@ -1,16 +1,24 @@
package proc
import (
"log"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
)
func TestProfile(t *testing.T) {
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()
// start again should not work
assert.NotNil(t, StartProfile())

View File

@@ -5,6 +5,7 @@ import (
"github.com/zeromicro/go-zero/core/load"
"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/stat"
"github.com/zeromicro/go-zero/core/trace"
@@ -56,6 +57,9 @@ func (sc ServiceConf) SetUp() error {
sc.Telemetry.Name = sc.Name
}
trace.StartAgent(sc.Telemetry)
proc.AddShutdownListener(func() {
trace.StopAgent()
})
if len(sc.MetricsUrl) > 0 {
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/proc"
"github.com/zeromicro/go-zero/core/sysx"
"github.com/zeromicro/go-zero/core/timex"
)
const (
@@ -47,14 +46,14 @@ func Report(msg string) {
if fn != nil {
reported := lessExecutor.DoOrDiscard(func() {
var builder strings.Builder
fmt.Fprintf(&builder, "%s\n", timex.Time().Format(timeFormat))
builder.WriteString(fmt.Sprintln(time.Now().Format(timeFormat)))
if len(clusterName) > 0 {
fmt.Fprintf(&builder, "cluster: %s\n", clusterName)
builder.WriteString(fmt.Sprintf("cluster: %s\n", clusterName))
}
fmt.Fprintf(&builder, "host: %s\n", sysx.Hostname())
builder.WriteString(fmt.Sprintf("host: %s\n", sysx.Hostname()))
dp := atomic.SwapInt32(&dropped, 0)
if dp > 0 {
fmt.Fprintf(&builder, "dropped: %d\n", dp)
builder.WriteString(fmt.Sprintf("dropped: %d\n", dp))
}
builder.WriteString(strings.TrimSpace(msg))
fn(builder.String())

View File

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

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"sync"
"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.
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)
if err != nil {
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.
@@ -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,
// 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.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,
// 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)
}
// TakeWithExpireCtx takes the result from cache first, if not found,
// 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)
return c.doTake(ctx, val, key, func(v interface{}) error {
return query(v, expire)
@@ -239,7 +244,11 @@ func (c cacheNode) doTake(ctx context.Context, v interface{}, key string,
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.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 {
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)
}
func TestCacheNode_Take(t *testing.T) {
func TestCacheNode_SetWithExpire(t *testing.T) {
store, clean, err := redistest.CreateRedis()
assert.Nil(t, err)
defer clean()
@@ -100,8 +100,18 @@ func TestCacheNode_Take(t *testing.T) {
lock: new(sync.Mutex),
unstableExpiry: mathx.NewUnstable(expiryDeviation),
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
err = cn.Take(&str, "any", func(v interface{}) error {
*v.(*string) = "value"

View File

@@ -2,7 +2,7 @@ package clickhouse
import (
// 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"
)

View File

@@ -110,7 +110,9 @@ type (
Ttl(key string) (int, error)
TtlCtx(ctx context.Context, key string) (int, error)
Zadd(key string, score int64, value string) (bool, error)
ZaddFloat(key string, score float64, value string) (bool, error)
ZaddCtx(ctx context.Context, key string, score int64, value string) (bool, error)
ZaddFloatCtx(ctx context.Context, key string, score float64, value string) (bool, error)
Zadds(key string, ps ...redis.Pair) (int64, error)
ZaddsCtx(ctx context.Context, key string, ps ...redis.Pair) (int64, error)
Zcard(key string) (int, error)
@@ -787,13 +789,21 @@ func (cs clusterStore) Zadd(key string, score int64, value string) (bool, error)
return cs.ZaddCtx(context.Background(), key, score, value)
}
func (cs clusterStore) ZaddFloat(key string, score float64, value string) (bool, error) {
return cs.ZaddFloatCtx(context.Background(), key, score, value)
}
func (cs clusterStore) ZaddCtx(ctx context.Context, key string, score int64, value string) (bool, error) {
return cs.ZaddFloatCtx(ctx, key, float64(score), value)
}
func (cs clusterStore) ZaddFloatCtx(ctx context.Context, key string, score float64, value string) (bool, error) {
node, err := cs.getRedis(key)
if err != nil {
return false, err
}
return node.ZaddCtx(ctx, key, score, value)
return node.ZaddFloatCtx(ctx, key, score, value)
}
func (cs clusterStore) Zadds(key string, ps ...redis.Pair) (int64, error) {

View File

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

View File

@@ -8,18 +8,35 @@ import (
"github.com/zeromicro/go-zero/core/breaker"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/timex"
"github.com/zeromicro/go-zero/core/trace"
"go.mongodb.org/mongo-driver/mongo"
mopt "go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
"go.opentelemetry.io/otel"
tracesdk "go.opentelemetry.io/otel/trace"
)
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
@@ -66,12 +83,12 @@ type (
// 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{},
FindOneAndReplace(ctx context.Context, filter, 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{},
FindOneAndUpdate(ctx context.Context, filter, update interface{},
opts ...*mopt.FindOneAndUpdateOptions) (*mongo.SingleResult, error)
// Indexes returns the index view for this collection.
Indexes() mongo.IndexView
@@ -82,16 +99,16 @@ type (
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{},
ReplaceOne(ctx context.Context, filter, 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{},
UpdateByID(ctx context.Context, id, update interface{},
opts ...*mopt.UpdateOptions) (*mongo.UpdateResult, error)
// UpdateMany updates the provided documents.
UpdateMany(ctx context.Context, filter interface{}, update interface{},
UpdateMany(ctx context.Context, filter, 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{},
UpdateOne(ctx context.Context, filter, 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) (
@@ -120,341 +137,411 @@ func newCollection(collection *mongo.Collection, brk breaker.Breaker) Collection
func (c *decoratedCollection) Aggregate(ctx context.Context, pipeline interface{},
opts ...*mopt.AggregateOptions) (cur *mongo.Cursor, err error) {
ctx, span := startSpan(ctx)
defer span.End()
ctx, span := startSpan(ctx, aggregate)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
c.logDurationSimple("Aggregate", starTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, bulkWrite)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("BulkWrite", startTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, countDocuments)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("CountDocuments", startTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, deleteMany)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("DeleteMany", startTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, deleteOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("DeleteOne", startTime, err, filter)
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)
defer span.End()
ctx, span := startSpan(ctx, distinct)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("Distinct", startTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, estimatedDocumentCount)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("EstimatedDocumentCount", startTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, find)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("Find", startTime, err, filter)
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)
defer span.End()
ctx, span := startSpan(ctx, findOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("FindOne", startTime, err, filter)
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)
defer span.End()
ctx, span := startSpan(ctx, findOneAndDelete)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("FindOneAndDelete", startTime, err, filter)
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)
defer span.End()
ctx, span := startSpan(ctx, findOneAndReplace)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("FindOneAndReplace", startTime, err, filter, replacement)
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{},
func (c *decoratedCollection) FindOneAndUpdate(ctx context.Context, filter, update interface{},
opts ...*mopt.FindOneAndUpdateOptions) (res *mongo.SingleResult, err error) {
ctx, span := startSpan(ctx)
defer span.End()
ctx, span := startSpan(ctx, findOneAndUpdate)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("FindOneAndUpdate", startTime, err, filter, update)
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)
defer span.End()
ctx, span := startSpan(ctx, insertMany)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("InsertMany", startTime, err)
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)
defer span.End()
ctx, span := startSpan(ctx, insertOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("InsertOne", startTime, err, document)
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{},
func (c *decoratedCollection) ReplaceOne(ctx context.Context, filter, replacement interface{},
opts ...*mopt.ReplaceOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx)
defer span.End()
ctx, span := startSpan(ctx, replaceOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("ReplaceOne", startTime, err, filter, replacement)
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{},
func (c *decoratedCollection) UpdateByID(ctx context.Context, id, update interface{},
opts ...*mopt.UpdateOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx)
defer span.End()
ctx, span := startSpan(ctx, updateByID)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("UpdateByID", startTime, err, id, update)
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{},
func (c *decoratedCollection) UpdateMany(ctx context.Context, filter, update interface{},
opts ...*mopt.UpdateOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx)
defer span.End()
ctx, span := startSpan(ctx, updateMany)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDurationSimple("UpdateMany", startTime, err)
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{},
func (c *decoratedCollection) UpdateOne(ctx context.Context, filter, update interface{},
opts ...*mopt.UpdateOptions) (res *mongo.UpdateResult, err error) {
ctx, span := startSpan(ctx)
defer span.End()
ctx, span := startSpan(ctx, updateOne)
defer func() {
endSpan(span, err)
}()
err = c.brk.DoWithAcceptable(func() error {
startTime := timex.Now()
defer func() {
c.logDuration("UpdateOne", startTime, err, filter, update)
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(method string, startTime time.Duration, err error,
docs ...interface{}) {
func (c *decoratedCollection) logDuration(ctx context.Context, method string,
startTime time.Duration, err error, docs ...interface{}) {
duration := timex.Since(startTime)
content, e := json.Marshal(docs)
if e != nil {
logx.Error(err)
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() {
logx.WithDuration(duration).Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s) - %s",
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s) - %s",
c.name, method, err.Error(), string(content))
} else {
logx.WithDuration(duration).Infof("mongo(%s) - %s - fail(%s) - %s",
logger.Infof("mongo(%s) - %s - fail(%s) - %s",
c.name, method, err.Error(), string(content))
}
} else {
if duration > slowThreshold.Load() {
logx.WithDuration(duration).Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s",
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s",
c.name, method, string(content))
} else {
logx.WithDuration(duration).Infof("mongo(%s) - %s - ok - %s", c.name, method, string(content))
logger.Infof("mongo(%s) - %s - ok - %s", c.name, method, string(content))
}
}
}
func (c *decoratedCollection) logDurationSimple(method string, startTime time.Duration, err error) {
logDuration(c.name, method, startTime, err)
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 {
@@ -483,8 +570,3 @@ func acceptable(err error) bool {
err == session.ErrAbortTwice || err == session.ErrCommitAfterAbort ||
err == session.ErrUnackWCUnsupported || err == session.ErrSnapshotTransaction
}
func startSpan(ctx context.Context) (context.Context, tracesdk.Span) {
tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
return tracer.Start(ctx, spanName)
}

View File

@@ -3,12 +3,14 @@ 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"
@@ -18,10 +20,6 @@ import (
var errDummy = errors.New("dummy")
func init() {
logx.Disable()
}
func TestKeepPromise_accept(t *testing.T) {
p := new(mockPromise)
kp := keepablePromise{
@@ -108,14 +106,14 @@ func TestCollection_BulkWrite(t *testing.T) {
}
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}})},
)
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}})},
)
mongo.NewInsertOneModel().SetDocument(bson.D{{Key: "foo", Value: 1}}),
})
assert.Equal(t, errDummy, err)
})
}
@@ -206,7 +204,7 @@ func TestCollection_EstimatedDocumentCount(t *testing.T) {
})
}
func TestCollectionFind(t *testing.T) {
func TestCollection_Find(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
@@ -254,7 +252,7 @@ func TestCollectionFind(t *testing.T) {
})
}
func TestCollectionFindOne(t *testing.T) {
func TestCollection_FindOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
@@ -438,7 +436,7 @@ func TestCollection_InsertMany(t *testing.T) {
})
}
func TestCollection_Remove(t *testing.T) {
func TestCollection_DeleteOne(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
@@ -458,7 +456,7 @@ func TestCollection_Remove(t *testing.T) {
})
}
func TestCollectionRemoveAll(t *testing.T) {
func TestCollection_DeleteMany(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
defer mt.Close()
@@ -567,6 +565,63 @@ func TestCollection_UpdateMany(t *testing.T) {
})
}
func TestDecoratedCollection_LogDuration(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
@@ -590,15 +645,15 @@ func (d *dropBreaker) Allow() (breaker.Promise, error) {
return nil, errDummy
}
func (d *dropBreaker) Do(req func() error) error {
func (d *dropBreaker) Do(_ func() error) error {
return nil
}
func (d *dropBreaker) DoWithAcceptable(req func() error, acceptable breaker.Acceptable) error {
func (d *dropBreaker) DoWithAcceptable(_ func() error, _ breaker.Acceptable) error {
return errDummy
}
func (d *dropBreaker) DoWithFallback(req func() error, fallback func(err error) error) error {
func (d *dropBreaker) DoWithFallback(_ func() error, _ func(err error) error) error {
return nil
}

View File

@@ -11,6 +11,14 @@ import (
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 {
@@ -23,7 +31,8 @@ type (
wrappedSession struct {
mongo.Session
brk breaker.Breaker
name string
brk breaker.Breaker
}
)
@@ -66,7 +75,7 @@ func (m *Model) StartSession(opts ...*mopt.SessionOptions) (sess mongo.Session,
err = m.brk.DoWithAcceptable(func() error {
starTime := timex.Now()
defer func() {
logDuration(m.name, "StartSession", starTime, err)
logDuration(context.Background(), m.name, startSession, starTime, err)
}()
session, sessionErr := m.cli.StartSession(opts...)
@@ -76,11 +85,13 @@ func (m *Model) StartSession(opts ...*mopt.SessionOptions) (sess mongo.Session,
sess = &wrappedSession{
Session: session,
name: m.name,
brk: m.brk,
}
return nil
}, acceptable)
return
}
@@ -148,7 +159,7 @@ func (m *Model) FindOneAndDelete(ctx context.Context, v, filter interface{},
}
// FindOneAndReplace finds a single document and replaces it.
func (m *Model) FindOneAndReplace(ctx context.Context, v, filter interface{}, replacement interface{},
func (m *Model) FindOneAndReplace(ctx context.Context, v, filter, replacement interface{},
opts ...*mopt.FindOneAndReplaceOptions) error {
res, err := m.Collection.FindOneAndReplace(ctx, filter, replacement, opts...)
if err != nil {
@@ -159,7 +170,7 @@ func (m *Model) FindOneAndReplace(ctx context.Context, v, filter interface{}, re
}
// FindOneAndUpdate finds a single document and updates it.
func (m *Model) FindOneAndUpdate(ctx context.Context, v, filter interface{}, update interface{},
func (m *Model) FindOneAndUpdate(ctx context.Context, v, filter, update interface{},
opts ...*mopt.FindOneAndUpdateOptions) error {
res, err := m.Collection.FindOneAndUpdate(ctx, filter, update, opts...)
if err != nil {
@@ -169,33 +180,57 @@ func (m *Model) FindOneAndUpdate(ctx context.Context, v, filter interface{}, upd
return res.Decode(v)
}
func (w *wrappedSession) AbortTransaction(ctx context.Context) error {
ctx, span := startSpan(ctx)
defer span.End()
// 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)
}
func (w *wrappedSession) CommitTransaction(ctx context.Context) error {
ctx, span := startSpan(ctx)
defer span.End()
// 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)
defer span.End()
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)
@@ -203,11 +238,20 @@ func (w *wrappedSession) WithTransaction(
return
}
// EndSession implements the mongo.Session interface.
func (w *wrappedSession) EndSession(ctx context.Context) {
ctx, span := startSpan(ctx)
defer span.End()
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.brk.DoWithAcceptable(func() error {
w.Session.EndSession(ctx)
return nil
}, acceptable)

37
core/stores/mon/trace.go Normal file
View File

@@ -0,0 +1,37 @@
package mon
import (
"context"
"github.com/zeromicro/go-zero/core/trace"
"go.mongodb.org/mongo-driver/mongo"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
oteltrace "go.opentelemetry.io/otel/trace"
)
var mongoCmdAttributeKey = attribute.Key("mongo.cmd")
func startSpan(ctx context.Context, cmd string) (context.Context, oteltrace.Span) {
tracer := otel.GetTracerProvider().Tracer(trace.TraceName)
ctx, span := tracer.Start(ctx,
spanName,
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
span.SetAttributes(mongoCmdAttributeKey.String(cmd))
return ctx, span
}
func endSpan(span oteltrace.Span, err error) {
defer span.End()
if err == nil || err == mongo.ErrNoDocuments ||
err == mongo.ErrNilValue || err == mongo.ErrNilDocument {
span.SetStatus(codes.Ok, "")
return
}
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
}

View File

@@ -1,6 +1,7 @@
package mon
import (
"context"
"strings"
"time"
@@ -15,11 +16,12 @@ func FormatAddr(hosts []string) string {
return strings.Join(hosts, mongoAddrSep)
}
func logDuration(name, method string, startTime time.Duration, err error) {
func logDuration(ctx context.Context, name, method string, startTime time.Duration, err error) {
duration := timex.Since(startTime)
logger := logx.WithContext(ctx).WithDuration(duration)
if err != nil {
logx.WithDuration(duration).Infof("mongo(%s) - %s - fail(%s)", name, method, err.Error())
logger.Infof("mongo(%s) - %s - fail(%s)", name, method, err.Error())
} else {
logx.WithDuration(duration).Infof("mongo(%s) - %s - ok", name, method)
logger.Infof("mongo(%s) - %s - ok", name, method)
}
}

View File

@@ -1,9 +1,14 @@
package mon
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
)
func TestFormatAddrs(t *testing.T) {
@@ -33,3 +38,26 @@ func TestFormatAddrs(t *testing.T) {
assert.Equal(t, test.expect, FormatAddr(test.addrs))
}
}
func Test_logDuration(t *testing.T) {
var buf strings.Builder
w := logx.NewWriter(&buf)
o := logx.Reset()
logx.SetWriter(w)
defer func() {
logx.Reset()
logx.SetWriter(o)
}()
buf.Reset()
logDuration(context.Background(), "foo", "bar", time.Millisecond, nil)
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "bar")
buf.Reset()
logDuration(context.Background(), "foo", "bar", time.Millisecond, errors.New("bar"))
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "bar")
assert.Contains(t, buf.String(), "fail")
}

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