Compare commits
56 Commits
tools/goct
...
v1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07e3e14c0e | ||
|
|
34c5f6616c | ||
|
|
32600f2619 | ||
|
|
b07df1c344 | ||
|
|
a1fca3a1da | ||
|
|
9394e59597 | ||
|
|
f8adc71529 | ||
|
|
c05e03bb5a | ||
|
|
199e86050e | ||
|
|
1e2a12b3d6 | ||
|
|
922efbfc2d | ||
|
|
842c4d81cc | ||
|
|
2a335c7608 | ||
|
|
35edd6b19d | ||
|
|
36bbc6a2e2 | ||
|
|
e20ccdd011 | ||
|
|
c2ff00883a | ||
|
|
00db97fcc1 | ||
|
|
117c3a9069 | ||
|
|
172ff407f3 | ||
|
|
a242fec5e1 | ||
|
|
6286941ebf | ||
|
|
42e0a6f90c | ||
|
|
81ae7d36b5 | ||
|
|
944e76edb9 | ||
|
|
151768ef82 | ||
|
|
50581c7f5c | ||
|
|
54041ef9e4 | ||
|
|
5a9ae5ef02 | ||
|
|
19de13bb04 | ||
|
|
3ab4e82168 | ||
|
|
619e838513 | ||
|
|
423597a01c | ||
|
|
d84dfe1b20 | ||
|
|
87b7a1120d | ||
|
|
528af8a99d | ||
|
|
17fc68ac5a | ||
|
|
804a56bd14 | ||
|
|
88f60d7736 | ||
|
|
95b7a3d3ce | ||
|
|
d71c0da7b7 | ||
|
|
fd070fec91 | ||
|
|
4f22034342 | ||
|
|
b731aa38af | ||
|
|
bf996a1812 | ||
|
|
af7ce65244 | ||
|
|
952db71835 | ||
|
|
abd1fa96a9 | ||
|
|
5aedd9c076 | ||
|
|
ff230c4b1d | ||
|
|
02c95108b9 | ||
|
|
1ff541afe4 | ||
|
|
11a8cbc1e5 | ||
|
|
c063976822 | ||
|
|
cb707034ce | ||
|
|
f10db27efd |
@@ -1,3 +1,7 @@
|
||||
coverage:
|
||||
status:
|
||||
patch: true
|
||||
project: false # disabled because project coverage is not stable
|
||||
comment:
|
||||
layout: "flags, files"
|
||||
behavior: once
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
14
.github/workflows/go.yml
vendored
14
.github/workflows/go.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.19
|
||||
check-latest: true
|
||||
cache: true
|
||||
id: go
|
||||
@@ -47,13 +47,13 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
# use 1.18 to guarantee Go 1.18 compatibility
|
||||
go-version: 1.18
|
||||
# use 1.19 to guarantee Go 1.19 compatibility
|
||||
go-version: 1.19
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
|
||||
2
.github/workflows/issues.yml
vendored
2
.github/workflows/issues.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v6
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
days-before-issue-stale: 365
|
||||
days-before-issue-close: 90
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- 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.18.10.linux-amd64.tar.gz"
|
||||
goversion: "https://dl.google.com/go/go1.19.13.linux-amd64.tar.gz"
|
||||
project_path: "tools/goctl"
|
||||
binary_name: "goctl"
|
||||
extra_files: tools/goctl/readme.md tools/goctl/readme-cn.md
|
||||
2
.github/workflows/reviewdog.yml
vendored
2
.github/workflows/reviewdog.yml
vendored
@@ -5,7 +5,7 @@ jobs:
|
||||
name: runner / staticcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: reviewdog/action-staticcheck@v1
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
|
||||
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -1,102 +1,76 @@
|
||||
# Contributing
|
||||
# 🚀 Contributing to go-zero
|
||||
|
||||
Welcome to go-zero!
|
||||
Welcome to the go-zero community! We're thrilled to have you here. Contributing to our project is a fantastic way to be a part of the go-zero journey. Let's make this guide exciting and fun!
|
||||
|
||||
- [Before you get started](#before-you-get-started)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Community Expectations](#community-expectations)
|
||||
- [Getting started](#getting-started)
|
||||
- [Your First Contribution](#your-first-contribution)
|
||||
- [Find something to work on](#find-something-to-work-on)
|
||||
- [Find a good first topic](#find-a-good-first-topic)
|
||||
- [Work on an Issue](#work-on-an-issue)
|
||||
- [File an Issue](#file-an-issue)
|
||||
- [Contributor Workflow](#contributor-workflow)
|
||||
- [Creating Pull Requests](#creating-pull-requests)
|
||||
- [Code Review](#code-review)
|
||||
- [Testing](#testing)
|
||||
## 📜 Before You Dive In
|
||||
|
||||
# Before you get started
|
||||
### 🤝 Code of Conduct
|
||||
|
||||
## Code of Conduct
|
||||
Let's start on the right foot. Please take a moment to read and embrace our [Code of Conduct](/code-of-conduct.md). We're all about creating a welcoming and respectful environment.
|
||||
|
||||
Please make sure to read and observe our [Code of Conduct](/code-of-conduct.md).
|
||||
### 🌟 Community Expectations
|
||||
|
||||
## Community Expectations
|
||||
At go-zero, we're like a close-knit family, and we believe in creating a healthy, friendly, and productive atmosphere. It's all about sharing knowledge and building amazing things together.
|
||||
|
||||
go-zero is a community project driven by its community which strives to promote a healthy, friendly and productive environment.
|
||||
go-zero is a web and rpc framework written in Go. It's born to ensure the stability of the busy sites with resilient design. Builtin goctl greatly improves the development productivity.
|
||||
## 🚀 Getting Started
|
||||
|
||||
# Getting started
|
||||
Get your adventure rolling! Here's how to begin:
|
||||
|
||||
- Fork the repository on GitHub.
|
||||
- Make your changes on your fork repository.
|
||||
- Submit a PR.
|
||||
1. 🍴 **Fork the Repository**: Head over to the GitHub repository and fork it to your own space.
|
||||
|
||||
2. 🛠️ **Make Your Magic**: Work your magic in your forked repository. Create new features, squash bugs, or improve documentation - it's your world to conquer!
|
||||
|
||||
# Your First Contribution
|
||||
3. 🚀 **Submit a PR (Pull Request)**: When you're ready to unveil your creation, submit a Pull Request. We can't wait to see your awesome work!
|
||||
|
||||
We will help you to contribute in different areas like filing issues, developing features, fixing critical bugs and
|
||||
getting your work reviewed and merged.
|
||||
## 🌟 Your First Contribution
|
||||
|
||||
If you have questions about the development process,
|
||||
feel free to [file an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
|
||||
We're here to guide you on your quest to become a go-zero contributor. Whether you want to file issues, develop features, or tame some critical bugs, we've got you covered.
|
||||
|
||||
## Find something to work on
|
||||
If you have questions or need guidance at any stage, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose).
|
||||
|
||||
We are always in need of help, be it fixing documentation, reporting bugs or writing some code.
|
||||
Look at places where you feel best coding practices aren't followed, code refactoring is needed or tests are missing.
|
||||
Here is how you get started.
|
||||
## 🔍 Find Something to Work On
|
||||
|
||||
### Find a good first topic
|
||||
Ready to dive into the action? There are several ways to contribute:
|
||||
|
||||
[go-zero](https://github.com/zeromicro/go-zero) has beginner-friendly issues that provide a good first issue.
|
||||
For example, [go-zero](https://github.com/zeromicro/go-zero) has
|
||||
[help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) and
|
||||
[good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
labels for issues that should not need deep knowledge of the system.
|
||||
We can help new contributors who wish to work on such issues.
|
||||
### 💼 Find a Good First Topic
|
||||
|
||||
Another good way to contribute is to find a documentation improvement, such as a missing/broken link.
|
||||
Please see [Contributing](#contributing) below for the workflow.
|
||||
Discover easy-entry issues labeled as [help wanted](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [good first issue](https://github.com/zeromicro/go-zero/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These issues are perfect for newcomers and don't require deep knowledge of the system. We're here to assist you with these tasks.
|
||||
|
||||
#### Work on an issue
|
||||
### 🪄 Work on an Issue
|
||||
|
||||
When you are willing to take on an issue, just reply on the issue. The maintainer will assign it to you.
|
||||
Once you've picked an issue that excites you, let us know by commenting on it. Our maintainers will assign it to you, and you can embark on your mission!
|
||||
|
||||
### File an Issue
|
||||
### 📢 File an Issue
|
||||
|
||||
While we encourage everyone to contribute code, it is also appreciated when someone reports an issue.
|
||||
Reporting an issue is just as valuable as code contributions. If you discover a problem, don't hesitate to [open an issue](https://github.com/zeromicro/go-zero/issues/new/choose). Be sure to follow our guidelines when submitting an issue.
|
||||
|
||||
Please follow the prompted submission guidelines while opening an issue.
|
||||
## 🎯 Contributor Workflow
|
||||
|
||||
# Contributor Workflow
|
||||
Here's a rough guide to your contributor journey:
|
||||
|
||||
Please do not ever hesitate to ask a question or send a pull request.
|
||||
1. 🌱 Create a New Branch: Start by creating a topic branch, usually based on the 'master' branch. This is where your contribution will grow.
|
||||
|
||||
This is a rough outline of what a contributor's workflow looks like:
|
||||
2. 💡 Make Commits: Commit your work in logical units. Each commit should tell a story.
|
||||
|
||||
- Create a topic branch from where to base the contribution. This is usually master.
|
||||
- Make commits of logical units.
|
||||
- Push changes in a topic branch to a personal fork of the repository.
|
||||
- Submit a pull request to [go-zero](https://github.com/zeromicro/go-zero).
|
||||
3. 🚀 Push Changes: Push the changes in your topic branch to your personal fork of the repository.
|
||||
|
||||
## Creating Pull Requests
|
||||
4. 📦 Submit a Pull Request: When your creation is complete, submit a Pull Request to the [go-zero repository](https://github.com/zeromicro/go-zero).
|
||||
|
||||
Pull requests are often called simply "PR".
|
||||
go-zero generally follows the standard [github pull request](https://help.github.com/articles/about-pull-requests/) process.
|
||||
To submit a proposed change, please develop the code/fix and add new test cases.
|
||||
After that, run these local verifications before submitting pull request to predict the pass or
|
||||
fail of continuous integration.
|
||||
## 🌠 Creating Pull Requests
|
||||
|
||||
* Format the code with `gofmt`
|
||||
* Run the test with data race enabled `go test -race ./...`
|
||||
Pull Requests (PRs) are your way of making a grand entrance with your contribution. Here's how to do it:
|
||||
|
||||
## Code Review
|
||||
- 💼 Format Your Code: Ensure your code is beautifully formatted with `gofmt`.
|
||||
- 🏃 Run Tests: Verify that your changes pass all the tests, including data race tests. Run `go test -race ./...` for the ultimate validation.
|
||||
|
||||
To make it easier for your PR to receive reviews, consider the reviewers will need you to:
|
||||
## 👁️🗨️ Code Review
|
||||
|
||||
* follow [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
* write [good commit messages](https://chris.beams.io/posts/git-commit/).
|
||||
* break large changes into a logical series of smaller patches which individually make easily understandable changes, and in aggregate solve a broader issue.
|
||||
Getting your PR reviewed is the final step before your contribution becomes part of go-zero's magical world. To make the process smooth, keep these things in mind:
|
||||
|
||||
- 🧙♀️ Follow Good Coding Practices: Stick to [good coding guidelines](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
- 📝 Write Awesome Commit Messages: Craft [impressive commit messages](https://chris.beams.io/posts/git-commit/) - they're like spells in the wizard's book!
|
||||
- 🔍 Break It Down: For larger changes, consider breaking them into a series of smaller, logical patches. Each patch should make an understandable and meaningful improvement.
|
||||
|
||||
Congratulations on your contribution journey! We're thrilled to have you as part of our go-zero community. Let's make amazing things together! 🌟
|
||||
|
||||
Now, go out there and start your adventure! If you have any more magical ideas to enhance this guide, please share them. 🔥
|
||||
@@ -46,7 +46,7 @@ type (
|
||||
// DoWithAcceptable returns an error instantly if the Breaker rejects the request.
|
||||
// If a panic occurs in the request, the Breaker handles it as an error
|
||||
// and causes the same panic again.
|
||||
// acceptable checks if it's a successful call, even if the err is not nil.
|
||||
// acceptable checks if it's a successful call, even if the error is not nil.
|
||||
DoWithAcceptable(req func() error, acceptable Acceptable) error
|
||||
|
||||
// DoWithFallback runs the given request if the Breaker accepts it.
|
||||
@@ -59,7 +59,7 @@ type (
|
||||
// DoWithFallbackAcceptable runs the fallback if the Breaker rejects the request.
|
||||
// If a panic occurs in the request, the Breaker handles it as an error
|
||||
// and causes the same panic again.
|
||||
// acceptable checks if it's a successful call, even if the err is not nil.
|
||||
// acceptable checks if it's a successful call, even if the error is not nil.
|
||||
DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error,
|
||||
}
|
||||
|
||||
func (lt loggedThrottle) logError(err error) error {
|
||||
if err == ErrServiceUnavailable {
|
||||
if errors.Is(err, ErrServiceUnavailable) {
|
||||
// if circuit open, not possible to have empty error window
|
||||
stat.Report(fmt.Sprintf(
|
||||
"proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s",
|
||||
|
||||
@@ -59,7 +59,7 @@ func GetBreaker(name string) Breaker {
|
||||
// NoBreakerFor disables the circuit breaker for the given name.
|
||||
func NoBreakerFor(name string) {
|
||||
lock.Lock()
|
||||
breakers[name] = newNoOpBreaker()
|
||||
breakers[name] = newNopBreaker()
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
assert.Equal(t, errDummy, GetBreaker("anyone").DoWithAcceptable(func() error {
|
||||
return errDummy
|
||||
}, func(err error) bool {
|
||||
return err == nil || err == errDummy
|
||||
return err == nil || errors.Is(err, errDummy)
|
||||
}))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
@@ -45,12 +45,12 @@ func TestBreakersDoWithAcceptable(t *testing.T) {
|
||||
}, func(err error) bool {
|
||||
return err == nil
|
||||
})
|
||||
assert.True(t, err == errDummy || err == ErrServiceUnavailable)
|
||||
assert.True(t, errors.Is(err, errDummy) || errors.Is(err, ErrServiceUnavailable))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return ErrServiceUnavailable == Do("another", func() error {
|
||||
return errors.Is(Do("another", func() error {
|
||||
return nil
|
||||
})
|
||||
}), ErrServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,12 +75,12 @@ func TestBreakersFallback(t *testing.T) {
|
||||
}, func(err error) error {
|
||||
return nil
|
||||
})
|
||||
assert.True(t, err == nil || err == errDummy)
|
||||
assert.True(t, err == nil || errors.Is(err, errDummy))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return ErrServiceUnavailable == Do("fallback", func() error {
|
||||
return errors.Is(Do("fallback", func() error {
|
||||
return nil
|
||||
})
|
||||
}), ErrServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,12 +94,12 @@ func TestBreakersAcceptableFallback(t *testing.T) {
|
||||
}, func(err error) bool {
|
||||
return err == nil
|
||||
})
|
||||
assert.True(t, err == nil || err == errDummy)
|
||||
assert.True(t, err == nil || errors.Is(err, errDummy))
|
||||
}
|
||||
verify(t, func() bool {
|
||||
return ErrServiceUnavailable == Do("acceptablefallback", func() error {
|
||||
return errors.Is(Do("acceptablefallback", func() error {
|
||||
return nil
|
||||
})
|
||||
}), ErrServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestGoogleBreakerAcceptable(t *testing.T) {
|
||||
assert.Equal(t, errAcceptable, b.doReq(func() error {
|
||||
return errAcceptable
|
||||
}, nil, func(err error) bool {
|
||||
return err == errAcceptable
|
||||
return errors.Is(err, errAcceptable)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestGoogleBreakerNotAcceptable(t *testing.T) {
|
||||
assert.Equal(t, errAcceptable, b.doReq(func() error {
|
||||
return errAcceptable
|
||||
}, nil, func(err error) bool {
|
||||
return err != errAcceptable
|
||||
return !errors.Is(err, errAcceptable)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
package breaker
|
||||
|
||||
const noOpBreakerName = "nopBreaker"
|
||||
const nopBreakerName = "nopBreaker"
|
||||
|
||||
type noOpBreaker struct{}
|
||||
type nopBreaker struct{}
|
||||
|
||||
func newNoOpBreaker() Breaker {
|
||||
return noOpBreaker{}
|
||||
func newNopBreaker() Breaker {
|
||||
return nopBreaker{}
|
||||
}
|
||||
|
||||
func (b noOpBreaker) Name() string {
|
||||
return noOpBreakerName
|
||||
func (b nopBreaker) Name() string {
|
||||
return nopBreakerName
|
||||
}
|
||||
|
||||
func (b noOpBreaker) Allow() (Promise, error) {
|
||||
func (b nopBreaker) Allow() (Promise, error) {
|
||||
return nopPromise{}, nil
|
||||
}
|
||||
|
||||
func (b noOpBreaker) Do(req func() error) error {
|
||||
func (b nopBreaker) Do(req func() error) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b noOpBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
|
||||
func (b nopBreaker) DoWithAcceptable(req func() error, _ Acceptable) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b noOpBreaker) DoWithFallback(req func() error, _ func(err error) error) error {
|
||||
func (b nopBreaker) DoWithFallback(req func() error, _ func(err error) error) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
func (b noOpBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error,
|
||||
func (b nopBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error,
|
||||
_ Acceptable) error {
|
||||
return req()
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func TestNopBreaker(t *testing.T) {
|
||||
b := newNoOpBreaker()
|
||||
assert.Equal(t, noOpBreakerName, b.Name())
|
||||
b := newNopBreaker()
|
||||
assert.Equal(t, nopBreakerName, b.Name())
|
||||
p, err := b.Allow()
|
||||
assert.Nil(t, err)
|
||||
p.Accept()
|
||||
|
||||
@@ -2,7 +2,6 @@ package fx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/errorx"
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
|
||||
const defaultRetryTimes = 3
|
||||
|
||||
var errTimeout = errors.New("retry timeout")
|
||||
|
||||
type (
|
||||
// RetryOption defines the method to customize DoWithRetry.
|
||||
RetryOption func(*retryOptions)
|
||||
@@ -28,7 +25,7 @@ type (
|
||||
// and performs modification operations, it is best to lock them,
|
||||
// otherwise there may be data race issues
|
||||
func DoWithRetry(fn func() error, opts ...RetryOption) error {
|
||||
return retry(func(errChan chan error, retryCount int) {
|
||||
return retry(context.Background(), func(errChan chan error, retryCount int) {
|
||||
errChan <- fn()
|
||||
}, opts...)
|
||||
}
|
||||
@@ -40,12 +37,12 @@ func DoWithRetry(fn func() error, opts ...RetryOption) error {
|
||||
// otherwise there may be data race issues
|
||||
func DoWithRetryCtx(ctx context.Context, fn func(ctx context.Context, retryCount int) error,
|
||||
opts ...RetryOption) error {
|
||||
return retry(func(errChan chan error, retryCount int) {
|
||||
return retry(ctx, func(errChan chan error, retryCount int) {
|
||||
errChan <- fn(ctx, retryCount)
|
||||
}, opts...)
|
||||
}
|
||||
|
||||
func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) error {
|
||||
func retry(ctx context.Context, fn func(errChan chan error, retryCount int), opts ...RetryOption) error {
|
||||
options := newRetryOptions()
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
@@ -53,7 +50,6 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
|
||||
|
||||
var berr errorx.BatchError
|
||||
var cancelFunc context.CancelFunc
|
||||
ctx := context.Background()
|
||||
if options.timeout > 0 {
|
||||
ctx, cancelFunc = context.WithTimeout(ctx, options.timeout)
|
||||
defer cancelFunc()
|
||||
@@ -71,14 +67,14 @@ func retry(fn func(errChan chan error, retryCount int), opts ...RetryOption) err
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
berr.Add(errTimeout)
|
||||
berr.Add(ctx.Err())
|
||||
return berr.Err()
|
||||
}
|
||||
|
||||
if options.interval > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
berr.Add(errTimeout)
|
||||
berr.Add(ctx.Err())
|
||||
return berr.Err()
|
||||
case <-time.After(options.interval):
|
||||
}
|
||||
|
||||
@@ -98,19 +98,51 @@ func TestRetryWithInterval(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRetryCtx(t *testing.T) {
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 0 {
|
||||
return errors.New("any")
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return nil
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 1 {
|
||||
t.Run("with timeout", func(t *testing.T) {
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 0 {
|
||||
return errors.New("any")
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return errors.New("any ")
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
|
||||
assert.NotNil(t, DoWithRetryCtx(context.Background(), func(ctx context.Context, retryCount int) error {
|
||||
if retryCount == 1 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return errors.New("any ")
|
||||
}, WithTimeout(time.Millisecond*250), WithInterval(time.Millisecond*150)))
|
||||
})
|
||||
|
||||
t.Run("with deadline exceeded", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
|
||||
defer cancel()
|
||||
|
||||
var times int
|
||||
assert.Error(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
|
||||
times++
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
return errors.New("any")
|
||||
}, WithInterval(time.Millisecond*150)))
|
||||
assert.Equal(t, 1, times)
|
||||
})
|
||||
|
||||
t.Run("with deadline not exceeded", func(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*250))
|
||||
defer cancel()
|
||||
|
||||
var times int
|
||||
assert.NoError(t, DoWithRetryCtx(ctx, func(ctx context.Context, retryCount int) error {
|
||||
times++
|
||||
if times == defaultRetryTimes {
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
return errors.New("any")
|
||||
}))
|
||||
assert.Equal(t, defaultRetryTimes, times)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ type LogConf struct {
|
||||
- `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. It’s 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.
|
||||
- `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 though `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.
|
||||
|
||||
@@ -41,67 +41,99 @@ type richLogger struct {
|
||||
}
|
||||
|
||||
func (l *richLogger) Debug(v ...any) {
|
||||
l.debug(fmt.Sprint(v...))
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Debugf(format string, v ...any) {
|
||||
l.debug(fmt.Sprintf(format, v...))
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Debugv(v any) {
|
||||
l.debug(v)
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Debugw(msg string, fields ...LogField) {
|
||||
l.debug(msg, fields...)
|
||||
if shallLog(DebugLevel) {
|
||||
l.debug(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Error(v ...any) {
|
||||
l.err(fmt.Sprint(v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Errorf(format string, v ...any) {
|
||||
l.err(fmt.Sprintf(format, v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Errorv(v any) {
|
||||
l.err(v)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Errorw(msg string, fields ...LogField) {
|
||||
l.err(msg, fields...)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.err(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Info(v ...any) {
|
||||
l.info(fmt.Sprint(v...))
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Infof(format string, v ...any) {
|
||||
l.info(fmt.Sprintf(format, v...))
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Infov(v any) {
|
||||
l.info(v)
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Infow(msg string, fields ...LogField) {
|
||||
l.info(msg, fields...)
|
||||
if shallLog(InfoLevel) {
|
||||
l.info(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Slow(v ...any) {
|
||||
l.slow(fmt.Sprint(v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Slowf(format string, v ...any) {
|
||||
l.slow(fmt.Sprintf(format, v...))
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Slowv(v any) {
|
||||
l.slow(v)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) Sloww(msg string, fields ...LogField) {
|
||||
l.slow(msg, fields...)
|
||||
if shallLog(ErrorLevel) {
|
||||
l.slow(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *richLogger) WithCallerSkip(skip int) Logger {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -332,11 +333,13 @@ func wrapLevelWithColor(level string) string {
|
||||
|
||||
func writeJson(writer io.Writer, info any) {
|
||||
if content, err := json.Marshal(info); err != nil {
|
||||
log.Println(err.Error())
|
||||
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
|
||||
} else if writer == nil {
|
||||
log.Println(string(content))
|
||||
} else {
|
||||
writer.Write(append(content, '\n'))
|
||||
if _, err := writer.Write(append(content, '\n')); err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +387,7 @@ func writePlainValue(writer io.Writer, level string, val any, fields ...string)
|
||||
buf.WriteString(level)
|
||||
buf.WriteByte(plainEncodingSep)
|
||||
if err := json.NewEncoder(&buf).Encode(val); err != nil {
|
||||
log.Println(err.Error())
|
||||
log.Printf("err: %s\n\n%s", err.Error(), debug.Stack())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -126,9 +126,23 @@ func TestWriteJson(t *testing.T) {
|
||||
log.SetOutput(&buf)
|
||||
writeJson(nil, "foo")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
|
||||
buf.Reset()
|
||||
writeJson(hardToWriteWriter{}, "foo")
|
||||
assert.Contains(t, buf.String(), "write error")
|
||||
|
||||
buf.Reset()
|
||||
writeJson(nil, make(chan int))
|
||||
assert.Contains(t, buf.String(), "unsupported type")
|
||||
|
||||
buf.Reset()
|
||||
type C struct {
|
||||
RC func()
|
||||
}
|
||||
writeJson(nil, C{
|
||||
RC: func() {},
|
||||
})
|
||||
assert.Contains(t, buf.String(), "runtime/debug.Stack")
|
||||
}
|
||||
|
||||
func TestWritePlainAny(t *testing.T) {
|
||||
@@ -165,6 +179,14 @@ func TestWritePlainAny(t *testing.T) {
|
||||
writePlainAny(hardToWriteWriter{}, levelFatal, "foo")
|
||||
assert.Contains(t, buf.String(), "write error")
|
||||
|
||||
buf.Reset()
|
||||
type C struct {
|
||||
RC func()
|
||||
}
|
||||
writePlainAny(nil, levelError, C{
|
||||
RC: func() {},
|
||||
})
|
||||
assert.Contains(t, buf.String(), "runtime/debug.Stack")
|
||||
}
|
||||
|
||||
func TestLogWithLimitContentLength(t *testing.T) {
|
||||
|
||||
@@ -19,9 +19,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultKeyName = "key"
|
||||
delimiter = '.'
|
||||
ignoreKey = "-"
|
||||
defaultKeyName = "key"
|
||||
delimiter = '.'
|
||||
ignoreKey = "-"
|
||||
numberTypeString = "number"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -622,7 +623,7 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
|
||||
}
|
||||
|
||||
if fValue > math.MaxFloat32 {
|
||||
return float32OverflowError(v.String())
|
||||
return fmt.Errorf("parsing %q as float32: value out of range", v.String())
|
||||
}
|
||||
|
||||
target.SetFloat(fValue)
|
||||
@@ -634,7 +635,7 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
|
||||
|
||||
target.SetFloat(fValue)
|
||||
default:
|
||||
return newTypeMismatchErrorWithHint(fullName, typeKind.String(), value.Type().String())
|
||||
return newTypeMismatchErrorWithHint(fullName, typeKind.String(), numberTypeString)
|
||||
}
|
||||
|
||||
SetValue(fieldType, value, target)
|
||||
|
||||
@@ -5490,7 +5490,7 @@ func TestUnmarshalerProcessFieldPrimitiveWithJSONNumber(t *testing.T) {
|
||||
err := m.processFieldPrimitiveWithJSONNumber(fieldType, value.Elem(), v,
|
||||
&fieldOptionsWithContext{}, "field")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, `type mismatch for field "field", expect "string", actual "int"`, err.Error())
|
||||
assert.Equal(t, `type mismatch for field "field", expect "string", actual "number"`, err.Error())
|
||||
})
|
||||
|
||||
t.Run("right type", func(t *testing.T) {
|
||||
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
leftSquareBracket = '['
|
||||
rightSquareBracket = ']'
|
||||
segmentSeparator = ','
|
||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -42,10 +43,6 @@ var (
|
||||
)
|
||||
|
||||
type (
|
||||
integer interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
|
||||
}
|
||||
|
||||
optionsCacheValue struct {
|
||||
key string
|
||||
options *fieldOptions
|
||||
@@ -104,38 +101,30 @@ func convertTypeFromString(kind reflect.Kind, str string) (any, error) {
|
||||
default:
|
||||
return false, errTypeMismatch
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
intValue, err := strconv.ParseInt(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return intValue, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
uintValue, err := strconv.ParseUint(str, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return uintValue, nil
|
||||
case reflect.Int:
|
||||
return strconv.ParseInt(str, 10, intSize)
|
||||
case reflect.Int8:
|
||||
return strconv.ParseInt(str, 10, 8)
|
||||
case reflect.Int16:
|
||||
return strconv.ParseInt(str, 10, 16)
|
||||
case reflect.Int32:
|
||||
return strconv.ParseInt(str, 10, 32)
|
||||
case reflect.Int64:
|
||||
return strconv.ParseInt(str, 10, 64)
|
||||
case reflect.Uint:
|
||||
return strconv.ParseUint(str, 10, intSize)
|
||||
case reflect.Uint8:
|
||||
return strconv.ParseUint(str, 10, 8)
|
||||
case reflect.Uint16:
|
||||
return strconv.ParseUint(str, 10, 16)
|
||||
case reflect.Uint32:
|
||||
return strconv.ParseUint(str, 10, 32)
|
||||
case reflect.Uint64:
|
||||
return strconv.ParseUint(str, 10, 64)
|
||||
case reflect.Float32:
|
||||
floatValue, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if floatValue > math.MaxFloat32 {
|
||||
return 0, float32OverflowError(str)
|
||||
}
|
||||
|
||||
return floatValue, nil
|
||||
return strconv.ParseFloat(str, 32)
|
||||
case reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return floatValue, nil
|
||||
return strconv.ParseFloat(str, 64)
|
||||
case reflect.String:
|
||||
return str, nil
|
||||
default:
|
||||
@@ -230,10 +219,6 @@ func implicitValueRequiredStruct(tag string, tp reflect.Type) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func intOverflowError[T integer](v T, kind reflect.Kind) error {
|
||||
return fmt.Errorf("parsing \"%d\" as %s: value out of range", v, kind.String())
|
||||
}
|
||||
|
||||
func isLeftInclude(b byte) (bool, error) {
|
||||
switch b {
|
||||
case '[':
|
||||
@@ -256,10 +241,6 @@ func isRightInclude(b byte) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func float32OverflowError(str string) error {
|
||||
return fmt.Errorf("parsing %q as float32: value out of range", str)
|
||||
}
|
||||
|
||||
func maybeNewValue(fieldType reflect.Type, value reflect.Value) {
|
||||
if fieldType.Kind() == reflect.Ptr && value.IsNil() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
@@ -505,41 +486,15 @@ func parseSegments(val string) []string {
|
||||
return segments
|
||||
}
|
||||
|
||||
func setIntValue(value reflect.Value, v any, min, max int64) error {
|
||||
iv := v.(int64)
|
||||
if iv < min || iv > max {
|
||||
return intOverflowError(iv, value.Kind())
|
||||
}
|
||||
|
||||
value.SetInt(iv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) error {
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
value.SetBool(v.(bool))
|
||||
return nil
|
||||
case reflect.Int: // int depends on int size, 32 or 64
|
||||
return setIntValue(value, v, math.MinInt, math.MaxInt)
|
||||
case reflect.Int8:
|
||||
return setIntValue(value, v, math.MinInt8, math.MaxInt8)
|
||||
case reflect.Int16:
|
||||
return setIntValue(value, v, math.MinInt16, math.MaxInt16)
|
||||
case reflect.Int32:
|
||||
return setIntValue(value, v, math.MinInt32, math.MaxInt32)
|
||||
case reflect.Int64:
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
value.SetInt(v.(int64))
|
||||
return nil
|
||||
case reflect.Uint: // uint depends on int size, 32 or 64
|
||||
return setUintValue(value, v, math.MaxUint)
|
||||
case reflect.Uint8:
|
||||
return setUintValue(value, v, math.MaxUint8)
|
||||
case reflect.Uint16:
|
||||
return setUintValue(value, v, math.MaxUint16)
|
||||
case reflect.Uint32:
|
||||
return setUintValue(value, v, math.MaxUint32)
|
||||
case reflect.Uint64:
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
value.SetUint(v.(uint64))
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
@@ -553,16 +508,6 @@ func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) err
|
||||
}
|
||||
}
|
||||
|
||||
func setUintValue(value reflect.Value, v any, boundary uint64) error {
|
||||
iv := v.(uint64)
|
||||
if iv > boundary {
|
||||
return intOverflowError(iv, value.Kind())
|
||||
}
|
||||
|
||||
value.SetUint(iv)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
|
||||
@@ -1011,6 +1011,15 @@ func TestUnmarshalYamlMapRune(t *testing.T) {
|
||||
assert.Equal(t, rune(3), v.Machine["node3"])
|
||||
}
|
||||
|
||||
func TestUnmarshalYamlStringOfInt(t *testing.T) {
|
||||
text := `password: 123456`
|
||||
var v struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
reader := strings.NewReader(text)
|
||||
assert.Error(t, UnmarshalYamlReader(reader, &v))
|
||||
}
|
||||
|
||||
func TestUnmarshalYamlBadInput(t *testing.T) {
|
||||
var v struct {
|
||||
Any string
|
||||
|
||||
@@ -3,7 +3,6 @@ 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,20 +46,16 @@ func NewCounterVec(cfg *CounterVecOpts) CounterVec {
|
||||
return cv
|
||||
}
|
||||
|
||||
func (cv *promCounterVec) Inc(labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
cv.counter.WithLabelValues(labels...).Inc()
|
||||
func (cv *promCounterVec) Add(v float64, labels ...string) {
|
||||
update(func() {
|
||||
cv.counter.WithLabelValues(labels...).Add(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (cv *promCounterVec) Add(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
cv.counter.WithLabelValues(labels...).Add(v)
|
||||
func (cv *promCounterVec) Inc(labels ...string) {
|
||||
update(func() {
|
||||
cv.counter.WithLabelValues(labels...).Inc()
|
||||
})
|
||||
}
|
||||
|
||||
func (cv *promCounterVec) close() bool {
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 (
|
||||
@@ -16,8 +15,12 @@ type (
|
||||
Set(v float64, labels ...string)
|
||||
// Inc increments labels.
|
||||
Inc(labels ...string)
|
||||
// Dec decrements labels.
|
||||
Dec(labels ...string)
|
||||
// Add adds v to labels.
|
||||
Add(v float64, labels ...string)
|
||||
// Sub subtracts v to labels.
|
||||
Sub(v float64, labels ...string)
|
||||
close() bool
|
||||
}
|
||||
|
||||
@@ -32,13 +35,12 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
|
||||
return nil
|
||||
}
|
||||
|
||||
vec := prom.NewGaugeVec(
|
||||
prom.GaugeOpts{
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
}, cfg.Labels)
|
||||
vec := prom.NewGaugeVec(prom.GaugeOpts{
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
}, cfg.Labels)
|
||||
prom.MustRegister(vec)
|
||||
gv := &promGaugeVec{
|
||||
gauge: vec,
|
||||
@@ -50,28 +52,34 @@ func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec {
|
||||
return gv
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) Inc(labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
gv.gauge.WithLabelValues(labels...).Inc()
|
||||
func (gv *promGaugeVec) Add(v float64, labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Add(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) Add(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
func (gv *promGaugeVec) Dec(labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Dec()
|
||||
})
|
||||
}
|
||||
|
||||
gv.gauge.WithLabelValues(labels...).Add(v)
|
||||
func (gv *promGaugeVec) Inc(labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Inc()
|
||||
})
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) Set(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Set(v)
|
||||
})
|
||||
}
|
||||
|
||||
gv.gauge.WithLabelValues(labels...).Set(v)
|
||||
func (gv *promGaugeVec) Sub(v float64, labels ...string) {
|
||||
update(func() {
|
||||
gv.gauge.WithLabelValues(labels...).Sub(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (gv *promGaugeVec) close() bool {
|
||||
|
||||
@@ -40,6 +40,23 @@ func TestGaugeInc(t *testing.T) {
|
||||
assert.Equal(t, float64(2), r)
|
||||
}
|
||||
|
||||
func TestGaugeDec(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
Namespace: "rpc_client",
|
||||
Subsystem: "requests",
|
||||
Name: "duration_ms",
|
||||
Help: "rpc server requests duration(ms).",
|
||||
Labels: []string{"path"},
|
||||
})
|
||||
defer gaugeVec.close()
|
||||
gv, _ := gaugeVec.(*promGaugeVec)
|
||||
gv.Dec("/users")
|
||||
gv.Dec("/users")
|
||||
r := testutil.ToFloat64(gv.gauge)
|
||||
assert.Equal(t, float64(-2), r)
|
||||
}
|
||||
|
||||
func TestGaugeAdd(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
@@ -57,6 +74,23 @@ func TestGaugeAdd(t *testing.T) {
|
||||
assert.Equal(t, float64(20), r)
|
||||
}
|
||||
|
||||
func TestGaugeSub(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
Namespace: "rpc_client",
|
||||
Subsystem: "request",
|
||||
Name: "duration_ms",
|
||||
Help: "rpc server requests duration(ms).",
|
||||
Labels: []string{"path"},
|
||||
})
|
||||
defer gaugeVec.close()
|
||||
gv, _ := gaugeVec.(*promGaugeVec)
|
||||
gv.Sub(-100, "/classroom")
|
||||
gv.Sub(30, "/classroom")
|
||||
r := testutil.ToFloat64(gv.gauge)
|
||||
assert.Equal(t, float64(70), r)
|
||||
}
|
||||
|
||||
func TestGaugeSet(t *testing.T) {
|
||||
startAgent()
|
||||
gaugeVec := NewGaugeVec(&GaugeVecOpts{
|
||||
|
||||
@@ -3,24 +3,26 @@ 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 (
|
||||
// A HistogramVecOpts is a histogram vector options.
|
||||
HistogramVecOpts struct {
|
||||
Namespace string
|
||||
Subsystem string
|
||||
Name string
|
||||
Help string
|
||||
Labels []string
|
||||
Buckets []float64
|
||||
Namespace string
|
||||
Subsystem string
|
||||
Name string
|
||||
Help string
|
||||
Labels []string
|
||||
Buckets []float64
|
||||
ConstLabels map[string]string
|
||||
}
|
||||
|
||||
// A HistogramVec interface represents a histogram vector.
|
||||
HistogramVec interface {
|
||||
// Observe adds observation v to labels.
|
||||
Observe(v int64, labels ...string)
|
||||
// ObserveFloat allow to observe float64 values.
|
||||
ObserveFloat(v float64, labels ...string)
|
||||
close() bool
|
||||
}
|
||||
|
||||
@@ -36,11 +38,12 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
|
||||
}
|
||||
|
||||
vec := prom.NewHistogramVec(prom.HistogramOpts{
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
Buckets: cfg.Buckets,
|
||||
Namespace: cfg.Namespace,
|
||||
Subsystem: cfg.Subsystem,
|
||||
Name: cfg.Name,
|
||||
Help: cfg.Help,
|
||||
Buckets: cfg.Buckets,
|
||||
ConstLabels: cfg.ConstLabels,
|
||||
}, cfg.Labels)
|
||||
prom.MustRegister(vec)
|
||||
hv := &promHistogramVec{
|
||||
@@ -54,11 +57,15 @@ func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec {
|
||||
}
|
||||
|
||||
func (hv *promHistogramVec) Observe(v int64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
update(func() {
|
||||
hv.histogram.WithLabelValues(labels...).Observe(float64(v))
|
||||
})
|
||||
}
|
||||
|
||||
hv.histogram.WithLabelValues(labels...).Observe(float64(v))
|
||||
func (hv *promHistogramVec) ObserveFloat(v float64, labels ...string) {
|
||||
update(func() {
|
||||
hv.histogram.WithLabelValues(labels...).Observe(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (hv *promHistogramVec) close() bool {
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestNewHistogramVec(t *testing.T) {
|
||||
Help: "rpc server requests duration(ms).",
|
||||
Buckets: []float64{1, 2, 3},
|
||||
})
|
||||
defer histogramVec.close()
|
||||
defer histogramVec.(*promHistogramVec).close()
|
||||
histogramVecNil := NewHistogramVec(nil)
|
||||
assert.NotNil(t, histogramVec)
|
||||
assert.Nil(t, histogramVecNil)
|
||||
@@ -28,9 +28,10 @@ func TestHistogramObserve(t *testing.T) {
|
||||
Buckets: []float64{1, 2, 3},
|
||||
Labels: []string{"method"},
|
||||
})
|
||||
defer histogramVec.close()
|
||||
defer histogramVec.(*promHistogramVec).close()
|
||||
hv, _ := histogramVec.(*promHistogramVec)
|
||||
hv.Observe(2, "/Users")
|
||||
hv.ObserveFloat(1.1, "/Users")
|
||||
|
||||
metadata := `
|
||||
# HELP counts rpc server requests duration(ms).
|
||||
@@ -38,11 +39,11 @@ func TestHistogramObserve(t *testing.T) {
|
||||
`
|
||||
val := `
|
||||
counts_bucket{method="/Users",le="1"} 0
|
||||
counts_bucket{method="/Users",le="2"} 1
|
||||
counts_bucket{method="/Users",le="3"} 1
|
||||
counts_bucket{method="/Users",le="+Inf"} 1
|
||||
counts_sum{method="/Users"} 2
|
||||
counts_count{method="/Users"} 1
|
||||
counts_bucket{method="/Users",le="2"} 2
|
||||
counts_bucket{method="/Users",le="3"} 2
|
||||
counts_bucket{method="/Users",le="+Inf"} 2
|
||||
counts_sum{method="/Users"} 3.1
|
||||
counts_count{method="/Users"} 2
|
||||
`
|
||||
|
||||
err := testutil.CollectAndCompare(hv.histogram, strings.NewReader(metadata+val))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package metric
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/prometheus"
|
||||
|
||||
// A VectorOpts is a general configuration.
|
||||
type VectorOpts struct {
|
||||
Namespace string
|
||||
@@ -8,3 +10,11 @@ type VectorOpts struct {
|
||||
Help string
|
||||
Labels []string
|
||||
}
|
||||
|
||||
func update(fn func()) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ 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,11 +52,9 @@ func NewSummaryVec(cfg *SummaryVecOpts) SummaryVec {
|
||||
}
|
||||
|
||||
func (sv *promSummaryVec) Observe(v float64, labels ...string) {
|
||||
if !prometheus.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
sv.summary.WithLabelValues(labels...).Observe(v)
|
||||
update(func() {
|
||||
sv.summary.WithLabelValues(labels...).Observe(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (sv *promSummaryVec) close() bool {
|
||||
|
||||
@@ -52,10 +52,10 @@ func WrapUp() {
|
||||
wrapUpListeners.notifyListeners()
|
||||
}
|
||||
|
||||
func gracefulStop(signals chan os.Signal) {
|
||||
func gracefulStop(signals chan os.Signal, sig syscall.Signal) {
|
||||
signal.Stop(signals)
|
||||
|
||||
logx.Info("Got signal SIGTERM, shutting down...")
|
||||
logx.Infof("Got signal %d, shutting down...", sig)
|
||||
go wrapUpListeners.notifyListeners()
|
||||
|
||||
time.Sleep(wrapUpTime)
|
||||
@@ -63,7 +63,7 @@ func gracefulStop(signals chan os.Signal) {
|
||||
|
||||
time.Sleep(delayTimeBeforeForceQuit - wrapUpTime)
|
||||
logx.Infof("Still alive after %v, going to force kill the process...", delayTimeBeforeForceQuit)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
_ = syscall.Kill(syscall.Getpid(), sig)
|
||||
}
|
||||
|
||||
type listenerManager struct {
|
||||
|
||||
@@ -20,7 +20,7 @@ func init() {
|
||||
|
||||
// https://golang.org/pkg/os/signal/#Notify
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM)
|
||||
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
for {
|
||||
v := <-signals
|
||||
@@ -35,14 +35,11 @@ func init() {
|
||||
profiler = nil
|
||||
}
|
||||
case syscall.SIGTERM:
|
||||
select {
|
||||
case <-done:
|
||||
// already closed
|
||||
default:
|
||||
close(done)
|
||||
}
|
||||
|
||||
gracefulStop(signals)
|
||||
stopOnSignal()
|
||||
gracefulStop(signals, syscall.SIGTERM)
|
||||
case syscall.SIGINT:
|
||||
stopOnSignal()
|
||||
gracefulStop(signals, syscall.SIGINT)
|
||||
default:
|
||||
logx.Error("Got unregistered signal:", v)
|
||||
}
|
||||
@@ -54,3 +51,12 @@ func init() {
|
||||
func Done() <-chan struct{} {
|
||||
return done
|
||||
}
|
||||
|
||||
func stopOnSignal() {
|
||||
select {
|
||||
case <-done:
|
||||
// already closed
|
||||
default:
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ func (t *Tree) Add(route string, item any) error {
|
||||
}
|
||||
|
||||
err := add(t.root, route[1:], item)
|
||||
switch err {
|
||||
case errDupItem:
|
||||
switch {
|
||||
case errors.Is(err, errDupItem):
|
||||
return duplicatedItem(route)
|
||||
case errDupSlash:
|
||||
case errors.Is(err, errDupSlash):
|
||||
return duplicatedSlash(route)
|
||||
default:
|
||||
return err
|
||||
|
||||
@@ -23,17 +23,22 @@ const (
|
||||
ProMode = "pro"
|
||||
)
|
||||
|
||||
// A ServiceConf is a service config.
|
||||
type ServiceConf struct {
|
||||
Name string
|
||||
Log logx.LogConf
|
||||
Mode string `json:",default=pro,options=dev|test|rt|pre|pro"`
|
||||
MetricsUrl string `json:",optional"`
|
||||
// Deprecated: please use DevServer
|
||||
Prometheus prometheus.Config `json:",optional"`
|
||||
Telemetry trace.Config `json:",optional"`
|
||||
DevServer devserver.Config `json:",optional"`
|
||||
}
|
||||
type (
|
||||
// DevServerConfig is type alias for devserver.Config
|
||||
DevServerConfig = devserver.Config
|
||||
|
||||
// A ServiceConf is a service config.
|
||||
ServiceConf struct {
|
||||
Name string
|
||||
Log logx.LogConf
|
||||
Mode string `json:",default=pro,options=dev|test|rt|pre|pro"`
|
||||
MetricsUrl string `json:",optional"`
|
||||
// Deprecated: please use DevServer
|
||||
Prometheus prometheus.Config `json:",optional"`
|
||||
Telemetry trace.Config `json:",optional"`
|
||||
DevServer DevServerConfig `json:",optional"`
|
||||
}
|
||||
)
|
||||
|
||||
// MustSetUp sets up the service, exits on error.
|
||||
func (sc ServiceConf) MustSetUp() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/internal/devserver"
|
||||
)
|
||||
|
||||
func TestServiceConf(t *testing.T) {
|
||||
@@ -14,6 +15,10 @@ func TestServiceConf(t *testing.T) {
|
||||
Mode: "console",
|
||||
},
|
||||
Mode: "dev",
|
||||
DevServer: devserver.Config{
|
||||
Port: 6470,
|
||||
HealthPath: "/healthz",
|
||||
},
|
||||
}
|
||||
c.MustSetUp()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
const dbTag = "db"
|
||||
|
||||
// RawFieldNames converts golang struct field into slice string.
|
||||
func RawFieldNames(in any, postgresSql ...bool) []string {
|
||||
func RawFieldNames(in any, postgreSql ...bool) []string {
|
||||
out := make([]string, 0)
|
||||
v := reflect.ValueOf(in)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
@@ -17,8 +17,8 @@ func RawFieldNames(in any, postgresSql ...bool) []string {
|
||||
}
|
||||
|
||||
var pg bool
|
||||
if len(postgresSql) > 0 {
|
||||
pg = postgresSql[0]
|
||||
if len(postgreSql) > 0 {
|
||||
pg = postgreSql[0]
|
||||
}
|
||||
|
||||
// we only accept structs
|
||||
|
||||
8
core/stores/cache/cachenode.go
vendored
8
core/stores/cache/cachenode.go
vendored
@@ -96,7 +96,7 @@ func (c cacheNode) Get(key string, val any) error {
|
||||
// GetCtx gets the cache with key and fills into v.
|
||||
func (c cacheNode) GetCtx(ctx context.Context, key string, val any) error {
|
||||
err := c.doGetCache(ctx, key, val)
|
||||
if err == errPlaceholder {
|
||||
if errors.Is(err, errPlaceholder) {
|
||||
return c.errNotFound
|
||||
}
|
||||
|
||||
@@ -210,16 +210,16 @@ func (c cacheNode) doTake(ctx context.Context, v any, key string,
|
||||
logger := logx.WithContext(ctx)
|
||||
val, fresh, err := c.barrier.DoEx(key, func() (any, error) {
|
||||
if err := c.doGetCache(ctx, key, v); err != nil {
|
||||
if err == errPlaceholder {
|
||||
if errors.Is(err, errPlaceholder) {
|
||||
return nil, c.errNotFound
|
||||
} else if err != c.errNotFound {
|
||||
} else if !errors.Is(err, c.errNotFound) {
|
||||
// why we just return the error instead of query from db,
|
||||
// because we don't allow the disaster pass to the dbs.
|
||||
// fail fast, in case we bring down the dbs.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = query(v); err == c.errNotFound {
|
||||
if err = query(v); errors.Is(err, c.errNotFound) {
|
||||
if err = c.setCacheWithNotFound(ctx, key); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package mon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/timex"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
mopt "go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -502,45 +501,11 @@ func (c *decoratedCollection) UpdateOne(ctx context.Context, filter, update any,
|
||||
|
||||
func (c *decoratedCollection) logDuration(ctx context.Context, method string,
|
||||
startTime time.Duration, err error, docs ...any) {
|
||||
duration := timex.Since(startTime)
|
||||
logger := logx.WithContext(ctx).WithDuration(duration)
|
||||
|
||||
content, jerr := json.Marshal(docs)
|
||||
// jerr should not be non-nil, but we don't care much on this,
|
||||
// if non-nil, we just log without docs.
|
||||
if jerr != nil {
|
||||
if err != nil {
|
||||
if duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s)", c.name, method, err.Error())
|
||||
} else {
|
||||
logger.Infof("mongo(%s) - %s - fail(%s)", c.name, method, err.Error())
|
||||
}
|
||||
} else {
|
||||
if duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok", c.name, method)
|
||||
} else {
|
||||
logger.Infof("mongo(%s) - %s - ok", c.name, method)
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
if duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - fail(%s) - %s",
|
||||
c.name, method, err.Error(), string(content))
|
||||
} else {
|
||||
logger.Infof("mongo(%s) - %s - fail(%s) - %s",
|
||||
c.name, method, err.Error(), string(content))
|
||||
}
|
||||
} else {
|
||||
if duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s",
|
||||
c.name, method, string(content))
|
||||
} else {
|
||||
logger.Infof("mongo(%s) - %s - ok - %s", c.name, method, string(content))
|
||||
}
|
||||
}
|
||||
logDurationWithDocs(ctx, c.name, method, startTime, err, docs...)
|
||||
}
|
||||
|
||||
func (c *decoratedCollection) logDurationSimple(ctx context.Context, method string, startTime time.Duration, err error) {
|
||||
func (c *decoratedCollection) logDurationSimple(ctx context.Context, method string,
|
||||
startTime time.Duration, err error) {
|
||||
logDuration(ctx, c.name, method, startTime, err)
|
||||
}
|
||||
|
||||
@@ -562,11 +527,19 @@ func (p keepablePromise) keep(err error) error {
|
||||
}
|
||||
|
||||
func acceptable(err error) bool {
|
||||
return err == nil || err == mongo.ErrNoDocuments || err == mongo.ErrNilValue ||
|
||||
err == mongo.ErrNilDocument || err == mongo.ErrNilCursor || err == mongo.ErrEmptySlice ||
|
||||
return err == nil ||
|
||||
errors.Is(err, mongo.ErrNoDocuments) ||
|
||||
errors.Is(err, mongo.ErrNilValue) ||
|
||||
errors.Is(err, mongo.ErrNilDocument) ||
|
||||
errors.Is(err, mongo.ErrNilCursor) ||
|
||||
errors.Is(err, mongo.ErrEmptySlice) ||
|
||||
// session errors
|
||||
err == session.ErrSessionEnded || err == session.ErrNoTransactStarted ||
|
||||
err == session.ErrTransactInProgress || err == session.ErrAbortAfterCommit ||
|
||||
err == session.ErrAbortTwice || err == session.ErrCommitAfterAbort ||
|
||||
err == session.ErrUnackWCUnsupported || err == session.ErrSnapshotTransaction
|
||||
errors.Is(err, session.ErrSessionEnded) ||
|
||||
errors.Is(err, session.ErrNoTransactStarted) ||
|
||||
errors.Is(err, session.ErrTransactInProgress) ||
|
||||
errors.Is(err, session.ErrAbortAfterCommit) ||
|
||||
errors.Is(err, session.ErrAbortTwice) ||
|
||||
errors.Is(err, session.ErrCommitAfterAbort) ||
|
||||
errors.Is(err, session.ErrUnackWCUnsupported) ||
|
||||
errors.Is(err, session.ErrSnapshotTransaction)
|
||||
}
|
||||
|
||||
@@ -599,13 +599,11 @@ func TestDecoratedCollection_LogDuration(t *testing.T) {
|
||||
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)
|
||||
|
||||
@@ -9,7 +9,11 @@ import (
|
||||
|
||||
const defaultTimeout = time.Second * 3
|
||||
|
||||
var slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
|
||||
var (
|
||||
slowThreshold = syncx.ForAtomicDuration(defaultSlowThreshold)
|
||||
logMon = syncx.ForAtomicBool(true)
|
||||
logSlowMon = syncx.ForAtomicBool(true)
|
||||
)
|
||||
|
||||
type (
|
||||
options = mopt.ClientOptions
|
||||
@@ -18,6 +22,17 @@ type (
|
||||
Option func(opts *options)
|
||||
)
|
||||
|
||||
// DisableLog disables logging of mongo commands, includes info and slow logs.
|
||||
func DisableLog() {
|
||||
logMon.Set(false)
|
||||
logSlowMon.Set(false)
|
||||
}
|
||||
|
||||
// DisableInfoLog disables info logging of mongo commands, but keeps slow logs.
|
||||
func DisableInfoLog() {
|
||||
logMon.Set(false)
|
||||
}
|
||||
|
||||
// SetSlowThreshold sets the slow threshold.
|
||||
func SetSlowThreshold(threshold time.Duration) {
|
||||
slowThreshold.Set(threshold)
|
||||
|
||||
@@ -25,3 +25,29 @@ func TestWithTimeout(t *testing.T) {
|
||||
WithTimeout(time.Second)(opts)
|
||||
assert.Equal(t, time.Second, *opts.Timeout)
|
||||
}
|
||||
|
||||
func TestDisableLog(t *testing.T) {
|
||||
assert.True(t, logMon.True())
|
||||
assert.True(t, logSlowMon.True())
|
||||
defer func() {
|
||||
logMon.Set(true)
|
||||
logSlowMon.Set(true)
|
||||
}()
|
||||
|
||||
DisableLog()
|
||||
assert.False(t, logMon.True())
|
||||
assert.False(t, logSlowMon.True())
|
||||
}
|
||||
|
||||
func TestDisableInfoLog(t *testing.T) {
|
||||
assert.True(t, logMon.True())
|
||||
assert.True(t, logSlowMon.True())
|
||||
defer func() {
|
||||
logMon.Set(true)
|
||||
logSlowMon.Set(true)
|
||||
}()
|
||||
|
||||
DisableInfoLog()
|
||||
assert.False(t, logMon.True())
|
||||
assert.True(t, logSlowMon.True())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package mon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/trace"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
@@ -23,8 +24,8 @@ func startSpan(ctx context.Context, cmd string) (context.Context, oteltrace.Span
|
||||
func endSpan(span oteltrace.Span, err error) {
|
||||
defer span.End()
|
||||
|
||||
if err == nil || err == mongo.ErrNoDocuments ||
|
||||
err == mongo.ErrNilValue || err == mongo.ErrNilDocument {
|
||||
if err == nil || errors.Is(err, mongo.ErrNoDocuments) ||
|
||||
errors.Is(err, mongo.ErrNilValue) || errors.Is(err, mongo.ErrNilDocument) {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package mon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -20,8 +21,41 @@ func logDuration(ctx context.Context, name, method string, startTime time.Durati
|
||||
duration := timex.Since(startTime)
|
||||
logger := logx.WithContext(ctx).WithDuration(duration)
|
||||
if err != nil {
|
||||
logger.Infof("mongo(%s) - %s - fail(%s)", name, method, err.Error())
|
||||
} else {
|
||||
logger.Errorf("mongo(%s) - %s - fail(%s)", name, method, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if logSlowMon.True() && duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok", name, method)
|
||||
} else if logMon.True() {
|
||||
logger.Infof("mongo(%s) - %s - ok", name, method)
|
||||
}
|
||||
}
|
||||
|
||||
func logDurationWithDocs(ctx context.Context, name, method string, startTime time.Duration,
|
||||
err error, docs ...any) {
|
||||
duration := timex.Since(startTime)
|
||||
logger := logx.WithContext(ctx).WithDuration(duration)
|
||||
|
||||
content, jerr := json.Marshal(docs)
|
||||
// jerr should not be non-nil, but we don't care much on this,
|
||||
// if non-nil, we just log without docs.
|
||||
if jerr != nil {
|
||||
if err != nil {
|
||||
logger.Errorf("mongo(%s) - %s - fail(%s)", name, method, err.Error())
|
||||
} else if logSlowMon.True() && duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok", name, method)
|
||||
} else if logMon.True() {
|
||||
logger.Infof("mongo(%s) - %s - ok", name, method)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("mongo(%s) - %s - fail(%s) - %s", name, method, err.Error(), string(content))
|
||||
} else if logSlowMon.True() && duration > slowThreshold.Load() {
|
||||
logger.Slowf("[MONGO] mongo(%s) - slowcall - %s - ok - %s", name, method, string(content))
|
||||
} else if logMon.True() {
|
||||
logger.Infof("mongo(%s) - %s - ok - %s", name, method, string(content))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/logx/logtest"
|
||||
"github.com/zeromicro/go-zero/core/timex"
|
||||
)
|
||||
|
||||
func TestFormatAddrs(t *testing.T) {
|
||||
@@ -42,13 +42,148 @@ func Test_logDuration(t *testing.T) {
|
||||
buf := logtest.NewCollector(t)
|
||||
|
||||
buf.Reset()
|
||||
logDuration(context.Background(), "foo", "bar", time.Millisecond, nil)
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil)
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "slow")
|
||||
|
||||
buf.Reset()
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now(), 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"))
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now(), errors.New("bar"))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "fail")
|
||||
|
||||
defer func() {
|
||||
logMon.Set(true)
|
||||
logSlowMon.Set(true)
|
||||
}()
|
||||
|
||||
buf.Reset()
|
||||
DisableInfoLog()
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now(), nil)
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil)
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "slow")
|
||||
|
||||
buf.Reset()
|
||||
DisableLog()
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now(), nil)
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil)
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDuration(context.Background(), "foo", "bar", timex.Now(), errors.New("bar"))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "fail")
|
||||
}
|
||||
|
||||
func Test_logDurationWithDoc(t *testing.T) {
|
||||
buf := logtest.NewCollector(t)
|
||||
buf.Reset()
|
||||
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil, make(chan int))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "slow")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil, "{'json': ''}")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "slow")
|
||||
assert.Contains(t, buf.String(), "json")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), nil, make(chan int))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), nil, "{'json': ''}")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "json")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), errors.New("bar"), make(chan int))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "fail")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), errors.New("bar"), "{'json': ''}")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "fail")
|
||||
assert.Contains(t, buf.String(), "json")
|
||||
|
||||
defer func() {
|
||||
logMon.Set(true)
|
||||
logSlowMon.Set(true)
|
||||
}()
|
||||
|
||||
buf.Reset()
|
||||
DisableInfoLog()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), nil, make(chan int))
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), nil, "{'json': ''}")
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil, make(chan int))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "slow")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil, "{'json': ''}")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "slow")
|
||||
assert.Contains(t, buf.String(), "json")
|
||||
|
||||
buf.Reset()
|
||||
DisableLog()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), nil, make(chan int))
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), nil, "{'json': ''}")
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil, make(chan int))
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now()-slowThreshold.Load()*2, nil, "{'json': ''}")
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), errors.New("bar"), make(chan int))
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "fail")
|
||||
|
||||
buf.Reset()
|
||||
logDurationWithDocs(context.Background(), "foo", "bar", timex.Now(), errors.New("bar"), "{'json': ''}")
|
||||
assert.Contains(t, buf.String(), "foo")
|
||||
assert.Contains(t, buf.String(), "bar")
|
||||
assert.Contains(t, buf.String(), "fail")
|
||||
assert.Contains(t, buf.String(), "json")
|
||||
}
|
||||
|
||||
@@ -54,9 +54,10 @@ func (h hook) AfterProcess(ctx context.Context, cmd red.Cmder) error {
|
||||
duration := timex.Since(start)
|
||||
if duration > slowThreshold.Load() {
|
||||
logDuration(ctx, []red.Cmder{cmd}, duration)
|
||||
metricSlowCount.Inc(cmd.Name())
|
||||
}
|
||||
|
||||
metricReqDur.Observe(duration.Milliseconds(), cmd.Name())
|
||||
metricReqDur.ObserveFloat(float64(duration)/float64(time.Millisecond), cmd.Name())
|
||||
if msg := formatError(err); len(msg) > 0 {
|
||||
metricReqErr.Inc(cmd.Name(), msg)
|
||||
}
|
||||
|
||||
@@ -52,19 +52,15 @@ func TestHookProcessCase2(t *testing.T) {
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
w := logtest.NewCollector(t)
|
||||
|
||||
ctx, err := durationHook.BeforeProcess(context.Background(), red.NewCmd(context.Background()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "redis", tracesdk.SpanFromContext(ctx).(interface{ Name() string }).Name())
|
||||
|
||||
time.Sleep(slowThreshold.Load() + time.Millisecond)
|
||||
|
||||
assert.Nil(t, durationHook.AfterProcess(ctx, red.NewCmd(context.Background(), "foo", "bar")))
|
||||
assert.True(t, strings.Contains(w.String(), "slow"))
|
||||
assert.True(t, strings.Contains(w.String(), "trace"))
|
||||
assert.True(t, strings.Contains(w.String(), "span"))
|
||||
}
|
||||
|
||||
func TestHookProcessCase3(t *testing.T) {
|
||||
@@ -89,6 +85,14 @@ func TestHookProcessCase4(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHookProcessPipelineCase1(t *testing.T) {
|
||||
ztrace.StartAgent(ztrace.Config{
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268/api/traces",
|
||||
Batcher: "jaeger",
|
||||
Sampler: 1.0,
|
||||
})
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
writer := log.Writer()
|
||||
var buf strings.Builder
|
||||
log.SetOutput(&buf)
|
||||
@@ -100,7 +104,6 @@ func TestHookProcessPipelineCase1(t *testing.T) {
|
||||
red.NewCmd(context.Background()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "redis", tracesdk.SpanFromContext(ctx).(interface{ Name() string }).Name())
|
||||
|
||||
assert.NoError(t, durationHook.AfterProcessPipeline(ctx, []red.Cmder{}))
|
||||
assert.NoError(t, durationHook.AfterProcessPipeline(ctx, []red.Cmder{
|
||||
@@ -119,12 +122,10 @@ func TestHookProcessPipelineCase2(t *testing.T) {
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
w := logtest.NewCollector(t)
|
||||
|
||||
ctx, err := durationHook.BeforeProcessPipeline(context.Background(), []red.Cmder{
|
||||
red.NewCmd(context.Background()),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "redis", tracesdk.SpanFromContext(ctx).(interface{ Name() string }).Name())
|
||||
|
||||
time.Sleep(slowThreshold.Load() + time.Millisecond)
|
||||
|
||||
@@ -132,8 +133,6 @@ func TestHookProcessPipelineCase2(t *testing.T) {
|
||||
red.NewCmd(context.Background(), "foo", "bar"),
|
||||
}))
|
||||
assert.True(t, strings.Contains(w.String(), "slow"))
|
||||
assert.True(t, strings.Contains(w.String(), "trace"))
|
||||
assert.True(t, strings.Contains(w.String(), "span"))
|
||||
}
|
||||
|
||||
func TestHookProcessPipelineCase3(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package redis
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/metric"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
red "github.com/go-redis/redis/v8"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/metric"
|
||||
)
|
||||
|
||||
const namespace = "redis_client"
|
||||
|
||||
@@ -11,7 +17,7 @@ var (
|
||||
Name: "duration_ms",
|
||||
Help: "redis client requests duration(ms).",
|
||||
Labels: []string{"command"},
|
||||
Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500},
|
||||
Buckets: []float64{0.25, 0.5, 1, 1.5, 2, 3, 5, 10, 25, 50, 100, 250, 500, 1000, 2000, 5000, 10000, 15000},
|
||||
})
|
||||
metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||
Namespace: namespace,
|
||||
@@ -20,4 +26,162 @@ var (
|
||||
Help: "redis client requests error count.",
|
||||
Labels: []string{"command", "error"},
|
||||
})
|
||||
metricSlowCount = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "requests",
|
||||
Name: "slow_total",
|
||||
Help: "redis client requests slow count.",
|
||||
Labels: []string{"command"},
|
||||
})
|
||||
|
||||
connLabels = []string{"key", "client_type"}
|
||||
connCollector = newCollector()
|
||||
_ prometheus.Collector = (*collector)(nil)
|
||||
)
|
||||
|
||||
type (
|
||||
statGetter struct {
|
||||
clientType string
|
||||
key string
|
||||
poolSize int
|
||||
poolStats func() *red.PoolStats
|
||||
}
|
||||
|
||||
// collector collects statistics from a redis client.
|
||||
// It implements the prometheus.Collector interface.
|
||||
collector struct {
|
||||
hitDesc *prometheus.Desc
|
||||
missDesc *prometheus.Desc
|
||||
timeoutDesc *prometheus.Desc
|
||||
totalDesc *prometheus.Desc
|
||||
idleDesc *prometheus.Desc
|
||||
staleDesc *prometheus.Desc
|
||||
maxDesc *prometheus.Desc
|
||||
|
||||
clients []*statGetter
|
||||
lock sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
func newCollector() *collector {
|
||||
c := &collector{
|
||||
hitDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_hit_total"),
|
||||
"Number of times a connection was found in the pool",
|
||||
connLabels, nil,
|
||||
),
|
||||
missDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_miss_total"),
|
||||
"Number of times a connection was not found in the pool",
|
||||
connLabels, nil,
|
||||
),
|
||||
timeoutDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_timeout_total"),
|
||||
"Number of times a timeout occurred when looking for a connection in the pool",
|
||||
connLabels, nil,
|
||||
),
|
||||
totalDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_conn_total_current"),
|
||||
"Current number of connections in the pool",
|
||||
connLabels, nil,
|
||||
),
|
||||
idleDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_conn_idle_current"),
|
||||
"Current number of idle connections in the pool",
|
||||
connLabels, nil,
|
||||
),
|
||||
staleDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_conn_stale_total"),
|
||||
"Number of times a connection was removed from the pool because it was stale",
|
||||
connLabels, nil,
|
||||
),
|
||||
maxDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "pool_conn_max"),
|
||||
"Max number of connections in the pool",
|
||||
connLabels, nil,
|
||||
),
|
||||
}
|
||||
|
||||
prometheus.MustRegister(c)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Describe implements the prometheus.Collector interface.
|
||||
func (s *collector) Describe(descs chan<- *prometheus.Desc) {
|
||||
descs <- s.hitDesc
|
||||
descs <- s.missDesc
|
||||
descs <- s.timeoutDesc
|
||||
descs <- s.totalDesc
|
||||
descs <- s.idleDesc
|
||||
descs <- s.staleDesc
|
||||
descs <- s.maxDesc
|
||||
}
|
||||
|
||||
// Collect implements the prometheus.Collector interface.
|
||||
func (s *collector) Collect(metrics chan<- prometheus.Metric) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
for _, client := range s.clients {
|
||||
key, clientType := client.key, client.clientType
|
||||
stats := client.poolStats()
|
||||
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.hitDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Hits),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.missDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Misses),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.timeoutDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Timeouts),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.totalDesc,
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.TotalConns),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.idleDesc,
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.IdleConns),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.staleDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.StaleConns),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
metrics <- prometheus.MustNewConstMetric(
|
||||
s.maxDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(client.poolSize),
|
||||
key,
|
||||
clientType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *collector) registerClient(client *statGetter) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.clients = append(s.clients, client)
|
||||
}
|
||||
|
||||
130
core/stores/redis/metrics_test.go
Normal file
130
core/stores/redis/metrics_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
red "github.com/go-redis/redis/v8"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"github.com/zeromicro/go-zero/internal/devserver"
|
||||
)
|
||||
|
||||
func TestRedisMetric(t *testing.T) {
|
||||
cfg := devserver.Config{}
|
||||
_ = conf.FillDefault(&cfg)
|
||||
server := devserver.NewServer(cfg)
|
||||
server.StartAsync()
|
||||
time.Sleep(time.Second)
|
||||
|
||||
metricReqDur.Observe(8, "test-cmd")
|
||||
metricReqErr.Inc("test-cmd", "internal-error")
|
||||
metricSlowCount.Inc("test-cmd")
|
||||
|
||||
url := "http://127.0.0.1:6060/metrics"
|
||||
resp, err := http.Get(url)
|
||||
assert.Nil(t, err)
|
||||
defer resp.Body.Close()
|
||||
s, err := io.ReadAll(resp.Body)
|
||||
assert.Nil(t, err)
|
||||
content := string(s)
|
||||
assert.Contains(t, content, "redis_client_requests_duration_ms_sum{command=\"test-cmd\"} 8\n")
|
||||
assert.Contains(t, content, "redis_client_requests_duration_ms_count{command=\"test-cmd\"} 1\n")
|
||||
assert.Contains(t, content, "redis_client_requests_error_total{command=\"test-cmd\",error=\"internal-error\"} 1\n")
|
||||
assert.Contains(t, content, "redis_client_requests_slow_total{command=\"test-cmd\"} 1\n")
|
||||
}
|
||||
|
||||
func Test_newCollector(t *testing.T) {
|
||||
prometheus.Unregister(connCollector)
|
||||
c := newCollector()
|
||||
c.registerClient(&statGetter{
|
||||
clientType: "node",
|
||||
key: "test1",
|
||||
poolSize: 10,
|
||||
poolStats: func() *red.PoolStats {
|
||||
return &red.PoolStats{
|
||||
Hits: 10000,
|
||||
Misses: 10,
|
||||
Timeouts: 5,
|
||||
TotalConns: 100,
|
||||
IdleConns: 20,
|
||||
StaleConns: 1,
|
||||
}
|
||||
},
|
||||
})
|
||||
c.registerClient(&statGetter{
|
||||
clientType: "node",
|
||||
key: "test2",
|
||||
poolSize: 11,
|
||||
poolStats: func() *red.PoolStats {
|
||||
return &red.PoolStats{
|
||||
Hits: 10001,
|
||||
Misses: 11,
|
||||
Timeouts: 6,
|
||||
TotalConns: 101,
|
||||
IdleConns: 21,
|
||||
StaleConns: 2,
|
||||
}
|
||||
},
|
||||
})
|
||||
c.registerClient(&statGetter{
|
||||
clientType: "cluster",
|
||||
key: "test3",
|
||||
poolSize: 5,
|
||||
poolStats: func() *red.PoolStats {
|
||||
return &red.PoolStats{
|
||||
Hits: 20000,
|
||||
Misses: 20,
|
||||
Timeouts: 10,
|
||||
TotalConns: 200,
|
||||
IdleConns: 40,
|
||||
StaleConns: 2,
|
||||
}
|
||||
},
|
||||
})
|
||||
val := `
|
||||
# HELP redis_client_pool_conn_idle_current Current number of idle connections in the pool
|
||||
# TYPE redis_client_pool_conn_idle_current gauge
|
||||
redis_client_pool_conn_idle_current{client_type="cluster",key="test3"} 40
|
||||
redis_client_pool_conn_idle_current{client_type="node",key="test1"} 20
|
||||
redis_client_pool_conn_idle_current{client_type="node",key="test2"} 21
|
||||
# HELP redis_client_pool_conn_max Max number of connections in the pool
|
||||
# TYPE redis_client_pool_conn_max counter
|
||||
redis_client_pool_conn_max{client_type="cluster",key="test3"} 5
|
||||
redis_client_pool_conn_max{client_type="node",key="test1"} 10
|
||||
redis_client_pool_conn_max{client_type="node",key="test2"} 11
|
||||
# HELP redis_client_pool_conn_stale_total Number of times a connection was removed from the pool because it was stale
|
||||
# TYPE redis_client_pool_conn_stale_total counter
|
||||
redis_client_pool_conn_stale_total{client_type="cluster",key="test3"} 2
|
||||
redis_client_pool_conn_stale_total{client_type="node",key="test1"} 1
|
||||
redis_client_pool_conn_stale_total{client_type="node",key="test2"} 2
|
||||
# HELP redis_client_pool_conn_total_current Current number of connections in the pool
|
||||
# TYPE redis_client_pool_conn_total_current gauge
|
||||
redis_client_pool_conn_total_current{client_type="cluster",key="test3"} 200
|
||||
redis_client_pool_conn_total_current{client_type="node",key="test1"} 100
|
||||
redis_client_pool_conn_total_current{client_type="node",key="test2"} 101
|
||||
# HELP redis_client_pool_hit_total Number of times a connection was found in the pool
|
||||
# TYPE redis_client_pool_hit_total counter
|
||||
redis_client_pool_hit_total{client_type="cluster",key="test3"} 20000
|
||||
redis_client_pool_hit_total{client_type="node",key="test1"} 10000
|
||||
redis_client_pool_hit_total{client_type="node",key="test2"} 10001
|
||||
# HELP redis_client_pool_miss_total Number of times a connection was not found in the pool
|
||||
# TYPE redis_client_pool_miss_total counter
|
||||
redis_client_pool_miss_total{client_type="cluster",key="test3"} 20
|
||||
redis_client_pool_miss_total{client_type="node",key="test1"} 10
|
||||
redis_client_pool_miss_total{client_type="node",key="test2"} 11
|
||||
# HELP redis_client_pool_timeout_total Number of times a timeout occurred when looking for a connection in the pool
|
||||
# TYPE redis_client_pool_timeout_total counter
|
||||
redis_client_pool_timeout_total{client_type="cluster",key="test3"} 10
|
||||
redis_client_pool_timeout_total{client_type="node",key="test1"} 5
|
||||
redis_client_pool_timeout_total{client_type="node",key="test2"} 6
|
||||
`
|
||||
|
||||
err := testutil.CollectAndCompare(c, strings.NewReader(val))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -2849,7 +2849,7 @@ func withHook(hook red.Hook) Option {
|
||||
}
|
||||
|
||||
func acceptable(err error) bool {
|
||||
return err == nil || err == red.Nil || err == context.Canceled
|
||||
return err == nil || err == red.Nil || errors.Is(err, context.Canceled)
|
||||
}
|
||||
|
||||
func getRedis(r *Redis) (RedisNode, error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package redis
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"runtime"
|
||||
|
||||
red "github.com/go-redis/redis/v8"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
@@ -14,7 +15,11 @@ const (
|
||||
idleConns = 8
|
||||
)
|
||||
|
||||
var clientManager = syncx.NewResourceManager()
|
||||
var (
|
||||
clientManager = syncx.NewResourceManager()
|
||||
// nodePoolSize is default pool size for node type of redis.
|
||||
nodePoolSize = 10 * runtime.GOMAXPROCS(0)
|
||||
)
|
||||
|
||||
func getClient(r *Redis) (*red.Client, error) {
|
||||
val, err := clientManager.GetResource(r.Addr, func() (io.Closer, error) {
|
||||
@@ -37,6 +42,15 @@ func getClient(r *Redis) (*red.Client, error) {
|
||||
store.AddHook(hook)
|
||||
}
|
||||
|
||||
connCollector.registerClient(&statGetter{
|
||||
clientType: NodeType,
|
||||
key: r.Addr,
|
||||
poolSize: nodePoolSize,
|
||||
poolStats: func() *red.PoolStats {
|
||||
return store.PoolStats()
|
||||
},
|
||||
})
|
||||
|
||||
return store, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package redis
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
red "github.com/go-redis/redis/v8"
|
||||
@@ -11,7 +12,11 @@ import (
|
||||
|
||||
const addrSep = ","
|
||||
|
||||
var clusterManager = syncx.NewResourceManager()
|
||||
var (
|
||||
clusterManager = syncx.NewResourceManager()
|
||||
// clusterPoolSize is default pool size for cluster type of redis.
|
||||
clusterPoolSize = 5 * runtime.GOMAXPROCS(0)
|
||||
)
|
||||
|
||||
func getCluster(r *Redis) (*red.ClusterClient, error) {
|
||||
val, err := clusterManager.GetResource(r.Addr, func() (io.Closer, error) {
|
||||
@@ -33,6 +38,15 @@ func getCluster(r *Redis) (*red.ClusterClient, error) {
|
||||
store.AddHook(hook)
|
||||
}
|
||||
|
||||
connCollector.registerClient(&statGetter{
|
||||
clientType: ClusterType,
|
||||
key: r.Addr,
|
||||
poolSize: clusterPoolSize,
|
||||
poolStats: func() *red.PoolStats {
|
||||
return store.PoolStats()
|
||||
},
|
||||
})
|
||||
|
||||
return store, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/executors"
|
||||
@@ -30,6 +31,7 @@ type (
|
||||
executor *executors.PeriodicalExecutor
|
||||
inserter *dbInserter
|
||||
stmt bulkStmt
|
||||
lock sync.RWMutex // guards stmt
|
||||
}
|
||||
|
||||
bulkStmt struct {
|
||||
@@ -65,6 +67,9 @@ func (bi *BulkInserter) Flush() {
|
||||
|
||||
// Insert inserts given args.
|
||||
func (bi *BulkInserter) Insert(args ...any) error {
|
||||
bi.lock.RLock()
|
||||
defer bi.lock.RUnlock()
|
||||
|
||||
value, err := format(bi.stmt.valueFormat, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -95,6 +100,11 @@ func (bi *BulkInserter) UpdateStmt(stmt string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bi.lock.Lock()
|
||||
defer bi.lock.Unlock()
|
||||
|
||||
// with write lock, it doesn't matter what's the order of setting bi.stmt and calling flush.
|
||||
bi.stmt = bkStmt
|
||||
bi.executor.Flush()
|
||||
bi.executor.Sync(func() {
|
||||
bi.inserter.stmt = bkStmt
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@@ -13,14 +16,19 @@ import (
|
||||
)
|
||||
|
||||
type mockedConn struct {
|
||||
query string
|
||||
args []any
|
||||
execErr error
|
||||
query string
|
||||
args []any
|
||||
execErr error
|
||||
updateCallback func(query string, args []any)
|
||||
}
|
||||
|
||||
func (c *mockedConn) ExecCtx(_ context.Context, query string, args ...any) (sql.Result, error) {
|
||||
c.query = query
|
||||
c.args = args
|
||||
if c.updateCallback != nil {
|
||||
c.updateCallback(query, args)
|
||||
}
|
||||
|
||||
return nil, c.execErr
|
||||
}
|
||||
|
||||
@@ -144,3 +152,50 @@ func TestBulkInserter_Update(t *testing.T) {
|
||||
assert.NotNil(t, inserter.UpdateStmt("foo"))
|
||||
assert.NotNil(t, inserter.Insert("foo", "bar"))
|
||||
}
|
||||
|
||||
func TestBulkInserter_UpdateStmt(t *testing.T) {
|
||||
var updated int32
|
||||
conn := mockedConn{
|
||||
execErr: errors.New("foo"),
|
||||
updateCallback: func(query string, args []any) {
|
||||
count := atomic.AddInt32(&updated, 1)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, 100, strings.Count(query, "foo"))
|
||||
if count == 1 {
|
||||
assert.Equal(t, 0, strings.Count(query, "bar"))
|
||||
} else {
|
||||
assert.Equal(t, 100, strings.Count(query, "bar"))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
inserter, err := NewBulkInserter(&conn, `INSERT INTO classroom_dau(classroom) VALUES(?)`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var wg1 sync.WaitGroup
|
||||
wg1.Add(2)
|
||||
for i := 0; i < 2; i++ {
|
||||
go func() {
|
||||
defer wg1.Done()
|
||||
for i := 0; i < 50; i++ {
|
||||
assert.NoError(t, inserter.Insert("foo"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg1.Wait()
|
||||
|
||||
assert.NoError(t, inserter.UpdateStmt(`INSERT INTO classroom_dau(classroom, user) VALUES(?, ?)`))
|
||||
|
||||
var wg2 sync.WaitGroup
|
||||
wg2.Add(1)
|
||||
go func() {
|
||||
defer wg2.Done()
|
||||
for i := 0; i < 100; i++ {
|
||||
assert.NoError(t, inserter.Insert("foo", "bar"))
|
||||
}
|
||||
inserter.Flush()
|
||||
}()
|
||||
wg2.Wait()
|
||||
|
||||
assert.Equal(t, int32(2), atomic.LoadInt32(&updated))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package sqlx
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/metric"
|
||||
import (
|
||||
"database/sql"
|
||||
"sync"
|
||||
|
||||
const namespace = "sql_client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/metric"
|
||||
)
|
||||
|
||||
const namespace = "mysql_client"
|
||||
|
||||
var (
|
||||
metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{
|
||||
@@ -11,7 +17,7 @@ var (
|
||||
Name: "duration_ms",
|
||||
Help: "mysql client requests duration(ms).",
|
||||
Labels: []string{"command"},
|
||||
Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500},
|
||||
Buckets: []float64{0.25, 0.5, 1, 1.5, 2, 3, 5, 10, 25, 50, 100, 250, 500, 1000, 2000, 5000, 10000, 15000},
|
||||
})
|
||||
metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||
Namespace: namespace,
|
||||
@@ -20,4 +26,145 @@ var (
|
||||
Help: "mysql client requests error count.",
|
||||
Labels: []string{"command", "error"},
|
||||
})
|
||||
metricSlowCount = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "requests",
|
||||
Name: "slow_total",
|
||||
Help: "mysql client requests slow count.",
|
||||
Labels: []string{"command"},
|
||||
})
|
||||
|
||||
connLabels = []string{"db_name", "hash"}
|
||||
connCollector = newCollector()
|
||||
_ prometheus.Collector = (*collector)(nil)
|
||||
)
|
||||
|
||||
type (
|
||||
statGetter struct {
|
||||
dbName string
|
||||
hash string
|
||||
poolStats func() sql.DBStats
|
||||
}
|
||||
|
||||
// collector collects statistics from a redis client.
|
||||
// It implements the prometheus.Collector interface.
|
||||
collector struct {
|
||||
maxOpenConnections *prometheus.Desc
|
||||
|
||||
openConnections *prometheus.Desc
|
||||
inUseConnections *prometheus.Desc
|
||||
idleConnections *prometheus.Desc
|
||||
|
||||
waitCount *prometheus.Desc
|
||||
waitDuration *prometheus.Desc
|
||||
maxIdleClosed *prometheus.Desc
|
||||
maxIdleTimeClosed *prometheus.Desc
|
||||
maxLifetimeClosed *prometheus.Desc
|
||||
|
||||
clients []*statGetter
|
||||
lock sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
func newCollector() *collector {
|
||||
c := &collector{
|
||||
maxOpenConnections: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "max_open_connections"),
|
||||
"Maximum number of open connections to the database.",
|
||||
connLabels, nil,
|
||||
),
|
||||
openConnections: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "open_connections"),
|
||||
"The number of established connections both in use and idle.",
|
||||
connLabels, nil,
|
||||
),
|
||||
inUseConnections: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "in_use_connections"),
|
||||
"The number of connections currently in use.",
|
||||
connLabels, nil,
|
||||
),
|
||||
idleConnections: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "idle_connections"),
|
||||
"The number of idle connections.",
|
||||
connLabels, nil,
|
||||
),
|
||||
waitCount: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "wait_count_total"),
|
||||
"The total number of connections waited for.",
|
||||
connLabels, nil,
|
||||
),
|
||||
waitDuration: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "wait_duration_seconds_total"),
|
||||
"The total time blocked waiting for a new connection.",
|
||||
connLabels, nil,
|
||||
),
|
||||
maxIdleClosed: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "max_idle_closed_total"),
|
||||
"The total number of connections closed due to SetMaxIdleConns.",
|
||||
connLabels, nil,
|
||||
),
|
||||
maxIdleTimeClosed: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "max_idle_time_closed_total"),
|
||||
"The total number of connections closed due to SetConnMaxIdleTime.",
|
||||
connLabels, nil,
|
||||
),
|
||||
maxLifetimeClosed: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, "", "max_lifetime_closed_total"),
|
||||
"The total number of connections closed due to SetConnMaxLifetime.",
|
||||
connLabels, nil,
|
||||
),
|
||||
}
|
||||
|
||||
prometheus.MustRegister(c)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Describe implements the prometheus.Collector interface.
|
||||
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.maxOpenConnections
|
||||
ch <- c.openConnections
|
||||
ch <- c.inUseConnections
|
||||
ch <- c.idleConnections
|
||||
ch <- c.waitCount
|
||||
ch <- c.waitDuration
|
||||
ch <- c.maxIdleClosed
|
||||
ch <- c.maxLifetimeClosed
|
||||
ch <- c.maxIdleTimeClosed
|
||||
}
|
||||
|
||||
// Collect implements the prometheus.Collector interface.
|
||||
func (c *collector) Collect(ch chan<- prometheus.Metric) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
for _, client := range c.clients {
|
||||
dbName, hash := client.dbName, client.hash
|
||||
stats := client.poolStats()
|
||||
ch <- prometheus.MustNewConstMetric(c.maxOpenConnections, prometheus.GaugeValue,
|
||||
float64(stats.MaxOpenConnections), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.openConnections, prometheus.GaugeValue,
|
||||
float64(stats.OpenConnections), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.inUseConnections, prometheus.GaugeValue,
|
||||
float64(stats.InUse), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.idleConnections, prometheus.GaugeValue,
|
||||
float64(stats.Idle), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.waitCount, prometheus.CounterValue,
|
||||
float64(stats.WaitCount), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.waitDuration, prometheus.CounterValue,
|
||||
stats.WaitDuration.Seconds(), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.maxIdleClosed, prometheus.CounterValue,
|
||||
float64(stats.MaxIdleClosed), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.maxLifetimeClosed, prometheus.CounterValue,
|
||||
float64(stats.MaxLifetimeClosed), dbName, hash)
|
||||
ch <- prometheus.MustNewConstMetric(c.maxIdleTimeClosed, prometheus.CounterValue,
|
||||
float64(stats.MaxIdleTimeClosed), dbName, hash)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *collector) registerClient(client *statGetter) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.clients = append(c.clients, client)
|
||||
}
|
||||
|
||||
147
core/stores/sqlx/metrics_test.go
Normal file
147
core/stores/sqlx/metrics_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package sqlx
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"github.com/zeromicro/go-zero/internal/devserver"
|
||||
)
|
||||
|
||||
func TestSqlxMetric(t *testing.T) {
|
||||
cfg := devserver.Config{}
|
||||
_ = conf.FillDefault(&cfg)
|
||||
cfg.Port = 6480
|
||||
server := devserver.NewServer(cfg)
|
||||
server.StartAsync()
|
||||
time.Sleep(time.Second)
|
||||
|
||||
metricReqDur.Observe(8, "test-cmd")
|
||||
metricReqErr.Inc("test-cmd", "internal-error")
|
||||
metricSlowCount.Inc("test-cmd")
|
||||
|
||||
url := "http://127.0.0.1:6480/metrics"
|
||||
resp, err := http.Get(url)
|
||||
assert.Nil(t, err)
|
||||
defer resp.Body.Close()
|
||||
s, err := io.ReadAll(resp.Body)
|
||||
assert.Nil(t, err)
|
||||
content := string(s)
|
||||
assert.Contains(t, content, "mysql_client_requests_duration_ms_sum{command=\"test-cmd\"} 8\n")
|
||||
assert.Contains(t, content, "mysql_client_requests_duration_ms_count{command=\"test-cmd\"} 1\n")
|
||||
assert.Contains(t, content, "mysql_client_requests_error_total{command=\"test-cmd\",error=\"internal-error\"} 1\n")
|
||||
assert.Contains(t, content, "mysql_client_requests_slow_total{command=\"test-cmd\"} 1\n")
|
||||
}
|
||||
|
||||
func TestMetricCollector(t *testing.T) {
|
||||
prometheus.Unregister(connCollector)
|
||||
c := newCollector()
|
||||
c.registerClient(&statGetter{
|
||||
dbName: "db-1",
|
||||
hash: "hash-1",
|
||||
poolStats: func() sql.DBStats {
|
||||
return sql.DBStats{
|
||||
MaxOpenConnections: 1,
|
||||
OpenConnections: 2,
|
||||
InUse: 3,
|
||||
Idle: 4,
|
||||
WaitCount: 5,
|
||||
WaitDuration: 6 * time.Second,
|
||||
MaxIdleClosed: 7,
|
||||
MaxIdleTimeClosed: 8,
|
||||
MaxLifetimeClosed: 9,
|
||||
}
|
||||
},
|
||||
})
|
||||
c.registerClient(&statGetter{
|
||||
dbName: "db-1",
|
||||
hash: "hash-2",
|
||||
poolStats: func() sql.DBStats {
|
||||
return sql.DBStats{
|
||||
MaxOpenConnections: 10,
|
||||
OpenConnections: 20,
|
||||
InUse: 30,
|
||||
Idle: 40,
|
||||
WaitCount: 50,
|
||||
WaitDuration: 60 * time.Second,
|
||||
MaxIdleClosed: 70,
|
||||
MaxIdleTimeClosed: 80,
|
||||
MaxLifetimeClosed: 90,
|
||||
}
|
||||
},
|
||||
})
|
||||
c.registerClient(&statGetter{
|
||||
dbName: "db-2",
|
||||
hash: "hash-2",
|
||||
poolStats: func() sql.DBStats {
|
||||
return sql.DBStats{
|
||||
MaxOpenConnections: 100,
|
||||
OpenConnections: 200,
|
||||
InUse: 300,
|
||||
Idle: 400,
|
||||
WaitCount: 500,
|
||||
WaitDuration: 600 * time.Second,
|
||||
MaxIdleClosed: 700,
|
||||
MaxIdleTimeClosed: 800,
|
||||
MaxLifetimeClosed: 900,
|
||||
}
|
||||
},
|
||||
})
|
||||
val := `
|
||||
# HELP mysql_client_idle_connections The number of idle connections.
|
||||
# TYPE mysql_client_idle_connections gauge
|
||||
mysql_client_idle_connections{db_name="db-1",hash="hash-1"} 4
|
||||
mysql_client_idle_connections{db_name="db-1",hash="hash-2"} 40
|
||||
mysql_client_idle_connections{db_name="db-2",hash="hash-2"} 400
|
||||
# HELP mysql_client_in_use_connections The number of connections currently in use.
|
||||
# TYPE mysql_client_in_use_connections gauge
|
||||
mysql_client_in_use_connections{db_name="db-1",hash="hash-1"} 3
|
||||
mysql_client_in_use_connections{db_name="db-1",hash="hash-2"} 30
|
||||
mysql_client_in_use_connections{db_name="db-2",hash="hash-2"} 300
|
||||
# HELP mysql_client_max_idle_closed_total The total number of connections closed due to SetMaxIdleConns.
|
||||
# TYPE mysql_client_max_idle_closed_total counter
|
||||
mysql_client_max_idle_closed_total{db_name="db-1",hash="hash-1"} 7
|
||||
mysql_client_max_idle_closed_total{db_name="db-1",hash="hash-2"} 70
|
||||
mysql_client_max_idle_closed_total{db_name="db-2",hash="hash-2"} 700
|
||||
# HELP mysql_client_max_idle_time_closed_total The total number of connections closed due to SetConnMaxIdleTime.
|
||||
# TYPE mysql_client_max_idle_time_closed_total counter
|
||||
mysql_client_max_idle_time_closed_total{db_name="db-1",hash="hash-1"} 8
|
||||
mysql_client_max_idle_time_closed_total{db_name="db-1",hash="hash-2"} 80
|
||||
mysql_client_max_idle_time_closed_total{db_name="db-2",hash="hash-2"} 800
|
||||
# HELP mysql_client_max_lifetime_closed_total The total number of connections closed due to SetConnMaxLifetime.
|
||||
# TYPE mysql_client_max_lifetime_closed_total counter
|
||||
mysql_client_max_lifetime_closed_total{db_name="db-1",hash="hash-1"} 9
|
||||
mysql_client_max_lifetime_closed_total{db_name="db-1",hash="hash-2"} 90
|
||||
mysql_client_max_lifetime_closed_total{db_name="db-2",hash="hash-2"} 900
|
||||
# HELP mysql_client_max_open_connections Maximum number of open connections to the database.
|
||||
# TYPE mysql_client_max_open_connections gauge
|
||||
mysql_client_max_open_connections{db_name="db-1",hash="hash-1"} 1
|
||||
mysql_client_max_open_connections{db_name="db-1",hash="hash-2"} 10
|
||||
mysql_client_max_open_connections{db_name="db-2",hash="hash-2"} 100
|
||||
# HELP mysql_client_open_connections The number of established connections both in use and idle.
|
||||
# TYPE mysql_client_open_connections gauge
|
||||
mysql_client_open_connections{db_name="db-1",hash="hash-1"} 2
|
||||
mysql_client_open_connections{db_name="db-1",hash="hash-2"} 20
|
||||
mysql_client_open_connections{db_name="db-2",hash="hash-2"} 200
|
||||
# HELP mysql_client_wait_count_total The total number of connections waited for.
|
||||
# TYPE mysql_client_wait_count_total counter
|
||||
mysql_client_wait_count_total{db_name="db-1",hash="hash-1"} 5
|
||||
mysql_client_wait_count_total{db_name="db-1",hash="hash-2"} 50
|
||||
mysql_client_wait_count_total{db_name="db-2",hash="hash-2"} 500
|
||||
# HELP mysql_client_wait_duration_seconds_total The total time blocked waiting for a new connection.
|
||||
# TYPE mysql_client_wait_duration_seconds_total counter
|
||||
mysql_client_wait_duration_seconds_total{db_name="db-1",hash="hash-1"} 6
|
||||
mysql_client_wait_duration_seconds_total{db_name="db-1",hash="hash-2"} 60
|
||||
mysql_client_wait_duration_seconds_total{db_name="db-2",hash="hash-2"} 600
|
||||
`
|
||||
|
||||
err := testutil.CollectAndCompare(c, strings.NewReader(val))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package sqlx
|
||||
|
||||
import "github.com/go-sql-driver/mysql"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
mysqlDriverName = "mysql"
|
||||
@@ -18,7 +22,8 @@ func mysqlAcceptable(err error) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
myerr, ok := err.(*mysql.MySQLError)
|
||||
var myerr *mysql.MySQLError
|
||||
ok := errors.As(err, &myerr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestBreakerOnNotHandlingDuplicateEntry(t *testing.T) {
|
||||
|
||||
var found bool
|
||||
for i := 0; i < 100; i++ {
|
||||
if tryOnDuplicateEntryError(t, nil) == breaker.ErrServiceUnavailable {
|
||||
if errors.Is(tryOnDuplicateEntryError(t, nil), breaker.ErrServiceUnavailable) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlx
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
@@ -157,7 +158,7 @@ func (db *commonSqlConn) ExecCtx(ctx context.Context, q string, args ...any) (
|
||||
result, err = exec(ctx, conn, q, args...)
|
||||
return err
|
||||
}, db.acceptable)
|
||||
if err == breaker.ErrServiceUnavailable {
|
||||
if errors.Is(err, breaker.ErrServiceUnavailable) {
|
||||
metricReqErr.Inc("Exec", "breaker")
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ func (db *commonSqlConn) PrepareCtx(ctx context.Context, query string) (stmt Stm
|
||||
}
|
||||
return nil
|
||||
}, db.acceptable)
|
||||
if err == breaker.ErrServiceUnavailable {
|
||||
if errors.Is(err, breaker.ErrServiceUnavailable) {
|
||||
metricReqErr.Inc("Prepare", "breaker")
|
||||
}
|
||||
|
||||
@@ -283,7 +284,7 @@ func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Contex
|
||||
err = db.brk.DoWithAcceptable(func() error {
|
||||
return transact(ctx, db, db.beginTx, fn)
|
||||
}, db.acceptable)
|
||||
if err == breaker.ErrServiceUnavailable {
|
||||
if errors.Is(err, breaker.ErrServiceUnavailable) {
|
||||
metricReqErr.Inc("Transact", "breaker")
|
||||
}
|
||||
|
||||
@@ -291,11 +292,13 @@ func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Contex
|
||||
}
|
||||
|
||||
func (db *commonSqlConn) acceptable(err error) bool {
|
||||
if err == nil || err == sql.ErrNoRows || err == sql.ErrTxDone || err == context.Canceled {
|
||||
if err == nil || errors.Is(err, sql.ErrNoRows) || errors.Is(err, sql.ErrTxDone) ||
|
||||
errors.Is(err, context.Canceled) {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := err.(acceptableError); ok {
|
||||
var e acceptableError
|
||||
if errors.As(err, &e) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -321,9 +324,9 @@ func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows)
|
||||
return qerr
|
||||
}, q, args...)
|
||||
}, func(err error) bool {
|
||||
return qerr == err || db.acceptable(err)
|
||||
return errors.Is(err, qerr) || db.acceptable(err)
|
||||
})
|
||||
if err == breaker.ErrServiceUnavailable {
|
||||
if errors.Is(err, breaker.ErrServiceUnavailable) {
|
||||
metricReqErr.Inc("queryRows", "breaker")
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ func (e *realSqlGuard) finish(ctx context.Context, err error) {
|
||||
duration := timex.Since(e.startTime)
|
||||
if duration > slowThreshold.Load() {
|
||||
logx.WithContext(ctx).WithDuration(duration).Slowf("[SQL] %s: slowcall - %s", e.command, e.stmt)
|
||||
metricSlowCount.Inc(e.command)
|
||||
} else if logSql.True() {
|
||||
logx.WithContext(ctx).WithDuration(duration).Infof("sql %s: %s", e.command, e.stmt)
|
||||
}
|
||||
@@ -136,7 +137,7 @@ func (e *realSqlGuard) finish(ctx context.Context, err error) {
|
||||
logSqlError(ctx, e.stmt, err)
|
||||
}
|
||||
|
||||
metricReqDur.Observe(duration.Milliseconds(), e.command)
|
||||
metricReqDur.ObserveFloat(float64(duration)/float64(time.Millisecond), e.command)
|
||||
}
|
||||
|
||||
func (e *realSqlGuard) start(q string, args ...any) error {
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlx
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/trace"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -23,7 +24,7 @@ func startSpan(ctx context.Context, method string) (context.Context, oteltrace.S
|
||||
func endSpan(span oteltrace.Span, err error) {
|
||||
defer span.End()
|
||||
|
||||
if err == nil || err == sql.ErrNoRows {
|
||||
if err == nil || errors.Is(err, sql.ErrNoRows) {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func logInstanceError(ctx context.Context, datasource string, err error) {
|
||||
}
|
||||
|
||||
func logSqlError(ctx context.Context, stmt string, err error) {
|
||||
if err != nil && err != ErrNotFound {
|
||||
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||
logx.WithContext(ctx).Errorf("stmt: %s, error: %s", stmt, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build go1.18
|
||||
|
||||
package stringx
|
||||
|
||||
import (
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestLockedCallDoErr(t *testing.T) {
|
||||
v, err := g.Do("key", func() (any, error) {
|
||||
return nil, someErr
|
||||
})
|
||||
if err != someErr {
|
||||
if !errors.Is(err, someErr) {
|
||||
t.Errorf("Do error = %v; want someErr", err)
|
||||
}
|
||||
if v != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestExclusiveCallDoErr(t *testing.T) {
|
||||
v, err := g.Do("key", func() (any, error) {
|
||||
return nil, someErr
|
||||
})
|
||||
if err != someErr {
|
||||
if !errors.Is(err, someErr) {
|
||||
t.Errorf("Do error = %v; want someErr", err)
|
||||
}
|
||||
if v != nil {
|
||||
|
||||
@@ -113,11 +113,13 @@ func createExporter(c Config) (sdktrace.SpanExporter, error) {
|
||||
}
|
||||
|
||||
func startAgent(c Config) error {
|
||||
AddResources(semconv.ServiceNameKey.String(c.Name))
|
||||
|
||||
opts := []sdktrace.TracerProviderOption{
|
||||
// Set the sampling rate based on the parent span to 100%
|
||||
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(c.Sampler))),
|
||||
// Record information about this application in a Resource.
|
||||
sdktrace.WithResource(resource.NewSchemaless(semconv.ServiceNameKey.String(c.Name))),
|
||||
sdktrace.WithResource(resource.NewSchemaless(attrResources...)),
|
||||
}
|
||||
|
||||
if len(c.Endpoint) > 0 {
|
||||
|
||||
10
core/trace/resource.go
Normal file
10
core/trace/resource.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package trace
|
||||
|
||||
import "go.opentelemetry.io/otel/attribute"
|
||||
|
||||
var attrResources = make([]attribute.KeyValue, 0)
|
||||
|
||||
// AddResources add more resources in addition to configured trace name.
|
||||
func AddResources(attrs ...attribute.KeyValue) {
|
||||
attrResources = append(attrResources, attrs...)
|
||||
}
|
||||
102
go.mod
102
go.mod
@@ -1,120 +1,120 @@
|
||||
module github.com/zeromicro/go-zero
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/alicebob/miniredis/v2 v2.30.5
|
||||
github.com/alicebob/miniredis/v2 v2.31.0
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/fullstorydev/grpcurl v1.8.8
|
||||
github.com/fullstorydev/grpcurl v1.8.9
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/jackc/pgx/v5 v5.4.3
|
||||
github.com/jhump/protoreflect v1.15.2
|
||||
github.com/jhump/protoreflect v1.15.3
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.etcd.io/etcd/api/v3 v3.5.9
|
||||
go.etcd.io/etcd/client/v3 v3.5.9
|
||||
go.etcd.io/etcd/api/v3 v3.5.10
|
||||
go.etcd.io/etcd/client/v3 v3.5.10
|
||||
go.mongodb.org/mongo-driver v1.12.1
|
||||
go.opentelemetry.io/otel v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.14.0
|
||||
go.opentelemetry.io/otel/sdk v1.14.0
|
||||
go.opentelemetry.io/otel/trace v1.14.0
|
||||
go.opentelemetry.io/otel v1.19.0
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.19.0
|
||||
go.opentelemetry.io/otel/sdk v1.19.0
|
||||
go.opentelemetry.io/otel/trace v1.19.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/net v0.15.0
|
||||
golang.org/x/sys v0.12.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e
|
||||
google.golang.org/grpc v1.58.2
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.26.3
|
||||
k8s.io/apimachinery v0.27.0-alpha.3
|
||||
k8s.io/client-go v0.26.3
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491
|
||||
k8s.io/api v0.28.3
|
||||
k8s.io/apimachinery v0.28.3
|
||||
k8s.io/client-go v0.28.3
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.6.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.1 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.15.15 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.1 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||
github.com/yuin/gopher-lua v1.1.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.19.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/term v0.12.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230307230338-69ee2d25a840 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
|
||||
@@ -122,7 +122,7 @@ GO111MODULE=on GOPROXY=https://goproxy.cn/,direct go get -u github.com/zeromicro
|
||||
# run goctl like
|
||||
docker run --rm -it -v `pwd`:/app kevinwan/goctl goctl --help
|
||||
|
||||
# docker for arm64 (M1) architecture
|
||||
# docker for arm64(Mac) architecture
|
||||
docker pull kevinwan/goctl:latest-arm64
|
||||
# run goctl like
|
||||
docker run --rm -it -v `pwd`:/app kevinwan/goctl:latest-arm64 goctl --help
|
||||
@@ -296,6 +296,7 @@ go-zero 已被许多公司用于生产部署,接入场景如在线教育、电
|
||||
>92. 深圳市万佳安物联科技股份有限公司
|
||||
>93. 武侯区编程之美软件开发工作室
|
||||
>94. 西安交通大学智慧能源与碳中和研究中心
|
||||
>95. 成都创软科技有限责任公司
|
||||
|
||||
如果贵公司也已使用 go-zero,欢迎在 [登记地址](https://github.com/zeromicro/go-zero/issues/602) 登记,仅仅为了推广,不做其它用途。
|
||||
|
||||
|
||||
86
readme.md
86
readme.md
@@ -31,41 +31,43 @@ go-zero contains simple API description syntax and code generation tool called `
|
||||
|
||||
#### Advantages of go-zero:
|
||||
|
||||
* improve the stability of the services with tens of millions of daily active users
|
||||
* builtin chained timeout control, concurrency control, rate limit, adaptive circuit breaker, adaptive load shedding, even no configuration needed
|
||||
* builtin middlewares also can be integrated into your frameworks
|
||||
* simple API syntax, one command to generate a couple of different languages
|
||||
* auto validate the request parameters from clients
|
||||
* plenty of builtin microservice management and concurrent toolkits
|
||||
* Improves the stability of the services with tens of millions of daily active users
|
||||
* Builtin chained timeout control, concurrency control, rate limit, adaptive circuit breaker, adaptive load shedding, even no configuration needed
|
||||
* Builtin middlewares also can be integrated into your frameworks
|
||||
* Simple API syntax, one command to generate a couple of different languages
|
||||
* Auto validate the request parameters from clients
|
||||
* Plenty of builtin microservice management and concurrent toolkits
|
||||
|
||||
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/architecture-en.png" alt="Architecture" width="1500" />
|
||||
|
||||
## Backgrounds of go-zero
|
||||
|
||||
At the beginning of 2018, we decided to re-design our system, from monolithic architecture with Java+MongoDB to microservice architecture. After research and comparison, we chose to:
|
||||
In early 2018, we embarked on a transformative journey to redesign our system, transitioning from a monolithic architecture built with Java and MongoDB to a microservices architecture. After careful research and comparison, we made a deliberate choice to:
|
||||
|
||||
* Golang based
|
||||
* great performance
|
||||
* simple syntax
|
||||
* proven engineering efficiency
|
||||
* extreme deployment experience
|
||||
* less server resource consumption
|
||||
* Self-designed microservice architecture
|
||||
* I have rich experience in designing microservice architectures
|
||||
* easy to locate the problems
|
||||
* easy to extend the features
|
||||
* Go Beyond with Golang
|
||||
* Great performance
|
||||
* Simple syntax
|
||||
* Proven engineering efficiency
|
||||
* Extreme deployment experience
|
||||
* Less server resource consumption
|
||||
|
||||
* Self-Design Our Microservice Architecture
|
||||
* Microservice architecture facilitates the creation of scalable, flexible, and maintainable software systems with independent, reusable components.
|
||||
* Easy to locate the problems within microservices.
|
||||
* Easy to extend the features by adding or modifying specific microservices without impacting the entire system.
|
||||
|
||||
## Design considerations on go-zero
|
||||
|
||||
By designing the microservice architecture, we expected to ensure stability, as well as productivity. And from just the beginning, we have the following design principles:
|
||||
|
||||
* keep it simple
|
||||
* high availability
|
||||
* stable on high concurrency
|
||||
* easy to extend
|
||||
* resilience design, failure-oriented programming
|
||||
* try best to be friendly to the business logic development, encapsulate the complexity
|
||||
* one thing, one way
|
||||
* Keep it simple
|
||||
* High availability
|
||||
* Stable on high concurrency
|
||||
* Easy to extend
|
||||
* Resilience design, failure-oriented programming
|
||||
* Try best to be friendly to the business logic development, encapsulate the complexity
|
||||
* One thing, one way
|
||||
|
||||
After almost half a year, we finished the transfer from a monolithic system to microservice system and deployed on August 2018. The new system guaranteed business growth and system stability.
|
||||
|
||||
@@ -73,19 +75,19 @@ After almost half a year, we finished the transfer from a monolithic system to m
|
||||
|
||||
go-zero is a web and rpc framework that integrates lots of engineering practices. The features are mainly listed below:
|
||||
|
||||
* powerful tool included, less code to write
|
||||
* simple interfaces
|
||||
* fully compatible with net/http
|
||||
* middlewares are supported, easy to extend
|
||||
* high performance
|
||||
* failure-oriented programming, resilience design
|
||||
* builtin service discovery, load balancing
|
||||
* builtin concurrency control, adaptive circuit breaker, adaptive load shedding, auto-trigger, auto recover
|
||||
* auto validation of API request parameters
|
||||
* chained timeout control
|
||||
* auto management of data caching
|
||||
* call tracing, metrics, and monitoring
|
||||
* high concurrency protected
|
||||
* Powerful tool included, less code to write
|
||||
* Simple interfaces
|
||||
* Fully compatible with net/http
|
||||
* Middlewares are supported, easy to extend
|
||||
* High performance
|
||||
* Failure-oriented programming, resilience design
|
||||
* Builtin service discovery, load balancing
|
||||
* Builtin concurrency control, adaptive circuit breaker, adaptive load shedding, auto-trigger, auto recover
|
||||
* Auto validation of API request parameters
|
||||
* Chained timeout control
|
||||
* Auto management of data caching
|
||||
* Call tracing, metrics, and monitoring
|
||||
* High concurrency protected
|
||||
|
||||
As below, go-zero protects the system with a couple of layers and mechanisms:
|
||||
|
||||
@@ -105,13 +107,13 @@ go get -u github.com/zeromicro/go-zero
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. full examples can be checked out from below:
|
||||
1. Full examples can be checked out from below:
|
||||
|
||||
[Rapid development of microservice systems](https://github.com/zeromicro/zero-doc/blob/main/doc/shorturl-en.md)
|
||||
|
||||
[Rapid development of microservice systems - multiple RPCs](https://github.com/zeromicro/zero-doc/blob/main/docs/zero/bookstore-en.md)
|
||||
|
||||
2. install goctl
|
||||
2. Install goctl
|
||||
|
||||
`goctl`can be read as `go control`. `goctl` means not to be controlled by code, instead, we control it. The inside `go` is not `golang`. At the very beginning, I was expecting it to help us improve productivity, and make our lives easier.
|
||||
|
||||
@@ -127,7 +129,7 @@ go get -u github.com/zeromicro/go-zero
|
||||
# run goctl like
|
||||
docker run --rm -it -v `pwd`:/app kevinwan/goctl goctl --help
|
||||
|
||||
# docker for arm64 (M1) architecture
|
||||
# docker for arm64(Mac) architecture
|
||||
docker pull kevinwan/goctl:latest-arm64
|
||||
# run goctl like
|
||||
docker run --rm -it -v `pwd`:/app kevinwan/goctl:latest-arm64 goctl --help
|
||||
@@ -135,7 +137,7 @@ go get -u github.com/zeromicro/go-zero
|
||||
|
||||
make sure goctl is executable.
|
||||
|
||||
3. create the API file, like greet.api, you can install the plugin of goctl in vs code, api syntax is supported.
|
||||
3. Create the API file, like greet.api, you can install the plugin of goctl in vs code, api syntax is supported.
|
||||
|
||||
```go
|
||||
type (
|
||||
@@ -160,7 +162,7 @@ go get -u github.com/zeromicro/go-zero
|
||||
goctl api -o greet.api
|
||||
```
|
||||
|
||||
4. generate the go server-side code
|
||||
4. Generate the go server-side code
|
||||
|
||||
```shell
|
||||
goctl api go -api greet.api -dir greet
|
||||
@@ -215,7 +217,7 @@ go get -u github.com/zeromicro/go-zero
|
||||
|
||||
5. Write the business logic code
|
||||
|
||||
* the dependencies can be passed into the logic within servicecontext.go, like mysql, reds, etc.
|
||||
* the dependencies can be passed into the logic within servicecontext.go, like mysql, redis, etc.
|
||||
* add the logic code in a logic package according to .api file
|
||||
|
||||
6. Generate code like Java, TypeScript, Dart, JavaScript, etc. just from the api file
|
||||
|
||||
69
rest/httpc/internal/metricsinterceptor.go
Normal file
69
rest/httpc/internal/metricsinterceptor.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/metric"
|
||||
"github.com/zeromicro/go-zero/core/timex"
|
||||
)
|
||||
|
||||
const clientNamespace = "httpc_client"
|
||||
|
||||
var (
|
||||
MetricClientReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{
|
||||
Namespace: clientNamespace,
|
||||
Subsystem: "requests",
|
||||
Name: "duration_ms",
|
||||
Help: "http client requests duration(ms).",
|
||||
Labels: []string{"name", "method", "url"},
|
||||
Buckets: []float64{0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2000, 5000, 10000, 15000},
|
||||
})
|
||||
|
||||
MetricClientReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||
Namespace: clientNamespace,
|
||||
Subsystem: "requests",
|
||||
Name: "code_total",
|
||||
Help: "http client requests code count.",
|
||||
Labels: []string{"name", "method", "url", "code"},
|
||||
})
|
||||
)
|
||||
|
||||
type MetricsURLRewriter func(u url.URL) string
|
||||
|
||||
func MetricsInterceptor(name string, pr MetricsURLRewriter) Interceptor {
|
||||
return func(r *http.Request) (*http.Request, ResponseHandler) {
|
||||
startTime := timex.Now()
|
||||
return r, func(resp *http.Response, err error) {
|
||||
var code int
|
||||
var path string
|
||||
|
||||
// error or resp is nil, set code=500
|
||||
if err != nil || resp == nil {
|
||||
code = http.StatusInternalServerError
|
||||
} else {
|
||||
code = resp.StatusCode
|
||||
}
|
||||
|
||||
u := cleanURL(*r.URL)
|
||||
method := r.Method
|
||||
if pr != nil {
|
||||
path = pr(u)
|
||||
} else {
|
||||
path = u.String()
|
||||
}
|
||||
|
||||
MetricClientReqDur.ObserveFloat(float64(timex.Since(startTime))/float64(time.Millisecond), name, method, path)
|
||||
MetricClientReqCodeTotal.Inc(name, method, path, strconv.Itoa(code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanURL(r url.URL) url.URL {
|
||||
r.RawQuery = ""
|
||||
r.RawFragment = ""
|
||||
r.User = nil
|
||||
return r
|
||||
}
|
||||
35
rest/httpc/internal/metricsinterceptor_test.go
Normal file
35
rest/httpc/internal/metricsinterceptor_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
func TestMetricsInterceptor(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
logx.Disable()
|
||||
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
|
||||
assert.NotNil(t, req)
|
||||
assert.Nil(t, err)
|
||||
interceptor := MetricsInterceptor("test", nil)
|
||||
req, handler := interceptor(req)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Nil(t, err)
|
||||
handler(resp, err)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package httpx
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
@@ -141,10 +142,10 @@ func doHandleError(w http.ResponseWriter, err error, handler func(error) (int, a
|
||||
return
|
||||
}
|
||||
|
||||
e, ok := body.(error)
|
||||
if ok {
|
||||
http.Error(w, e.Error(), code)
|
||||
} else {
|
||||
switch v := body.(type) {
|
||||
case error:
|
||||
http.Error(w, v.Error(), code)
|
||||
default:
|
||||
writeJson(w, code, body)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +163,7 @@ func doWriteJson(w http.ResponseWriter, code int, v any) error {
|
||||
if n, err := w.Write(bs); err != nil {
|
||||
// http.ErrHandlerTimeout has been handled by http.TimeoutHandler,
|
||||
// so it's ignored here.
|
||||
if err != http.ErrHandlerTimeout {
|
||||
if !errors.Is(err, http.ErrHandlerTimeout) {
|
||||
return fmt.Errorf("write response failed, error: %w", err)
|
||||
}
|
||||
} else if n < len(bs) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@@ -42,14 +43,14 @@ func start(host string, port int, handler http.Handler, run func(svr *http.Serve
|
||||
}
|
||||
healthManager := health.NewHealthManager(fmt.Sprintf("%s-%s:%d", probeNamePrefix, host, port))
|
||||
|
||||
waitForCalled := proc.AddWrapUpListener(func() {
|
||||
waitForCalled := proc.AddShutdownListener(func() {
|
||||
healthManager.MarkNotReady()
|
||||
if e := server.Shutdown(context.Background()); e != nil {
|
||||
logx.Error(e)
|
||||
}
|
||||
})
|
||||
defer func() {
|
||||
if err == http.ErrServerClosed {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
waitForCalled()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -2,6 +2,7 @@ package rest
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
@@ -307,7 +308,7 @@ func WithUnsignedCallback(callback handler.UnsignedCallback) RunOption {
|
||||
|
||||
func handleError(err error) {
|
||||
// ErrServerClosed means the server is closed manually
|
||||
if err == nil || err == http.ErrServerClosed {
|
||||
if err == nil || errors.Is(err, http.ErrServerClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/parser"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/pkg/env"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/rpc/execx"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||
)
|
||||
@@ -53,7 +54,10 @@ func TestParser(t *testing.T) {
|
||||
filename := "greet.api"
|
||||
err := os.WriteFile(filename, []byte(testApiTemplate), os.ModePerm)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(filename)
|
||||
env.Set(t, env.GoctlExperimental, "off")
|
||||
t.Cleanup(func() {
|
||||
os.Remove(filename)
|
||||
})
|
||||
|
||||
api, err := parser.Parse(filename)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -156,6 +156,7 @@ func (p *Parser) invokeImportedApi(filename string, imports []*ImportExpr) ([]*A
|
||||
}
|
||||
// ignore already imported file
|
||||
if p.alreadyImported(impPath) {
|
||||
p.importStatck.pop()
|
||||
continue
|
||||
}
|
||||
p.fileMap[impPath] = PlaceHolder{}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func TestFileSplitor(t *testing.T) {
|
||||
t.Skip("skip this test because it is used to split the apiparser_parser.go file by developer.")
|
||||
dir := "."
|
||||
data, err := os.ReadFile(filepath.Join(dir, "apiparser_parser.go"))
|
||||
if err != nil {
|
||||
|
||||
@@ -174,17 +174,15 @@ func (p parser) findDefinedType(name string) (*spec.Type, error) {
|
||||
}
|
||||
|
||||
func (p parser) fieldToMember(field *ast.TypeField) spec.Member {
|
||||
name := ""
|
||||
tag := ""
|
||||
var name string
|
||||
var tag string
|
||||
if !field.IsAnonymous {
|
||||
name = field.Name.Text()
|
||||
if field.Tag == nil {
|
||||
panic(fmt.Sprintf("error: line %d:%d field %s has no tag",
|
||||
field.Name.Line(), field.Name.Column(), field.Name.Text()))
|
||||
if field.Tag != nil {
|
||||
tag = field.Tag.Text()
|
||||
}
|
||||
|
||||
tag = field.Tag.Text()
|
||||
}
|
||||
|
||||
return spec.Member{
|
||||
Name: name,
|
||||
Type: p.astTypeToSpec(field.DataType),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/zeromicro/go-zero/tools/goctl
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/zeromicro/ddl-parser v1.0.5
|
||||
github.com/zeromicro/go-zero v1.5.6
|
||||
golang.org/x/text v0.13.0
|
||||
google.golang.org/grpc v1.58.2
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
)
|
||||
|
||||
@@ -90,15 +90,15 @@ require (
|
||||
go.uber.org/automaxprocs v1.5.3 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/term v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.11.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -118,7 +118,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -346,8 +346,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -407,16 +407,16 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -459,11 +459,11 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -580,10 +580,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g=
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -602,8 +602,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I=
|
||||
google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// BuildVersion is the version of goctl.
|
||||
const BuildVersion = "1.5.6"
|
||||
const BuildVersion = "1.6.0"
|
||||
|
||||
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-bata": 2, "beta": 3, "released": 4, "": 5}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ func init() {
|
||||
pgDatasourceCmdFlags.StringVar(&command.VarStringHome, "home")
|
||||
pgDatasourceCmdFlags.StringVar(&command.VarStringRemote, "remote")
|
||||
pgDatasourceCmdFlags.StringVar(&command.VarStringBranch, "branch")
|
||||
pgCmd.PersistentFlags().StringSliceVarPWithDefaultValue(&command.VarStringSliceIgnoreColumns,
|
||||
"ignore-columns", "i", []string{"create_at", "created_at", "create_time", "update_at", "updated_at", "update_time"})
|
||||
|
||||
mongoCmdFlags.StringSliceVarP(&mongo.VarStringSliceType, "type", "t")
|
||||
mongoCmdFlags.BoolVarP(&mongo.VarBoolCache, "cache", "c")
|
||||
@@ -67,7 +69,8 @@ func init() {
|
||||
mongoCmdFlags.StringVar(&mongo.VarStringBranch, "branch")
|
||||
|
||||
mysqlCmd.PersistentFlags().BoolVar(&command.VarBoolStrict, "strict")
|
||||
mysqlCmd.PersistentFlags().StringSliceVarPWithDefaultValue(&command.VarStringSliceIgnoreColumns, "ignore-columns", "i", []string{"create_at", "created_at", "create_time", "update_at", "updated_at", "update_time"})
|
||||
mysqlCmd.PersistentFlags().StringSliceVarPWithDefaultValue(&command.VarStringSliceIgnoreColumns,
|
||||
"ignore-columns", "i", []string{"create_at", "created_at", "create_time", "update_at", "updated_at", "update_time"})
|
||||
|
||||
mysqlCmd.AddCommand(datasourceCmd, ddlCmd)
|
||||
pgCmd.AddCommand(pgDatasourceCmd)
|
||||
|
||||
@@ -216,8 +216,9 @@ func PostgreSqlDataSource(_ *cobra.Command, _ []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ignoreColumns := mergeColumns(VarStringSliceIgnoreColumns)
|
||||
|
||||
return fromPostgreSqlDataSource(url, pattern, dir, schema, cfg, cache, idea, VarBoolStrict)
|
||||
return fromPostgreSqlDataSource(url, pattern, dir, schema, cfg, cache, idea, VarBoolStrict, ignoreColumns)
|
||||
}
|
||||
|
||||
type ddlArg struct {
|
||||
@@ -329,7 +330,7 @@ func fromMysqlDataSource(arg dataSourceArg) error {
|
||||
return generator.StartFromInformationSchema(matchTables, arg.cache, arg.strict)
|
||||
}
|
||||
|
||||
func fromPostgreSqlDataSource(url, pattern, dir, schema string, cfg *config.Config, cache, idea, strict bool) error {
|
||||
func fromPostgreSqlDataSource(url, pattern, dir, schema string, cfg *config.Config, cache, idea, strict bool, ignoreColumns []string) error {
|
||||
log := console.NewConsole(idea)
|
||||
if len(url) == 0 {
|
||||
log.Error("%v", "expected data source of postgresql, but nothing found")
|
||||
@@ -376,7 +377,7 @@ func fromPostgreSqlDataSource(url, pattern, dir, schema string, cfg *config.Conf
|
||||
return errors.New("no tables matched")
|
||||
}
|
||||
|
||||
generator, err := gen.NewDefaultGenerator(dir, cfg, gen.WithConsoleOption(log), gen.WithPostgreSql())
|
||||
generator, err := gen.NewDefaultGenerator(dir, cfg, gen.WithConsoleOption(log), gen.WithPostgreSql(), gen.WithIgnoreColumns(ignoreColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ func TestGenCacheKeys(t *testing.T) {
|
||||
VarExpression: `cacheGoZeroUserIdPrefix = "cache:goZero:user:id:"`,
|
||||
KeyLeft: "goZeroUserIdKey",
|
||||
KeyRight: `fmt.Sprintf("%s%v", cacheGoZeroUserIdPrefix, id)`,
|
||||
DataKeyRight: `fmt.Sprintf("%s%v", cacheGoZeroUserIdPrefix, data.ID)`,
|
||||
DataKeyRight: `fmt.Sprintf("%s%v", cacheGoZeroUserIdPrefix, data.Id)`,
|
||||
KeyExpression: `goZeroUserIdKey := fmt.Sprintf("%s%v", cacheGoZeroUserIdPrefix, id)`,
|
||||
DataKeyExpression: `goZeroUserIdKey := fmt.Sprintf("%s%v", cacheGoZeroUserIdPrefix, data.ID)`,
|
||||
DataKeyExpression: `goZeroUserIdKey := fmt.Sprintf("%s%v", cacheGoZeroUserIdPrefix, data.Id)`,
|
||||
FieldNameJoin: []string{"id"},
|
||||
})
|
||||
}())
|
||||
@@ -170,9 +170,9 @@ func TestGenCacheKeys(t *testing.T) {
|
||||
VarExpression: `cacheUserIdPrefix = "cache:user:id:"`,
|
||||
KeyLeft: "userIdKey",
|
||||
KeyRight: `fmt.Sprintf("%s%v", cacheUserIdPrefix, id)`,
|
||||
DataKeyRight: `fmt.Sprintf("%s%v", cacheUserIdPrefix, data.ID)`,
|
||||
DataKeyRight: `fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)`,
|
||||
KeyExpression: `userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)`,
|
||||
DataKeyExpression: `userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.ID)`,
|
||||
DataKeyExpression: `userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)`,
|
||||
FieldNameJoin: []string{"id"},
|
||||
})
|
||||
}())
|
||||
|
||||
@@ -5,9 +5,3 @@ func new{{.upperStartCamelObject}}Model(conn sqlx.SqlConn{{if .withCache}}, c ca
|
||||
}
|
||||
}
|
||||
|
||||
func (m *default{{.upperStartCamelObject}}Model) withSession(session sqlx.Session) *default{{.upperStartCamelObject}}Model {
|
||||
return &default{{.upperStartCamelObject}}Model{
|
||||
{{if .withCache}}CachedConn:m.CachedConn.WithSession(session){{else}}conn:sqlx.NewSqlConnFromSession(session){{end}},
|
||||
table: {{.table}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type (
|
||||
// and implement the added methods in custom{{.upperStartCamelObject}}Model.
|
||||
{{.upperStartCamelObject}}Model interface {
|
||||
{{.lowerStartCamelObject}}Model
|
||||
{{if not .withCache}}withSession(session sqlx.Session) {{.upperStartCamelObject}}Model{{end}}
|
||||
}
|
||||
|
||||
custom{{.upperStartCamelObject}}Model struct {
|
||||
@@ -28,3 +29,10 @@ func New{{.upperStartCamelObject}}Model(conn sqlx.SqlConn{{if .withCache}}, c ca
|
||||
default{{.upperStartCamelObject}}Model: new{{.upperStartCamelObject}}Model(conn{{if .withCache}}, c, opts...{{end}}),
|
||||
}
|
||||
}
|
||||
|
||||
{{if not .withCache}}
|
||||
func (m *custom{{.upperStartCamelObject}}Model) withSession(session sqlx.Session) {{.upperStartCamelObject}}Model {
|
||||
return New{{.upperStartCamelObject}}Model(sqlx.NewSqlConnFromSession(session))
|
||||
}
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestStudentModel(t *testing.T) {
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
err := mockStudent(func(mock sqlmock.Sqlmock) {
|
||||
err := mockStudent(t, func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectExec(fmt.Sprintf("insert into %s", testTable)).
|
||||
WithArgs(data.Class, data.Name, data.Age, data.Score).
|
||||
WillReturnResult(sqlmock.NewResult(testInsertId, testRowsAffected))
|
||||
@@ -61,7 +61,7 @@ func TestStudentModel(t *testing.T) {
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = mockStudent(func(mock sqlmock.Sqlmock) {
|
||||
err = mockStudent(t, func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectQuery(fmt.Sprintf("select (.+) from %s", testTable)).
|
||||
WithArgs(testInsertId).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "class", "name", "age", "score", "create_time", "update_time"}).AddRow(testInsertId, data.Class, data.Name, data.Age, data.Score, testTimeValue, testTimeValue))
|
||||
@@ -79,7 +79,7 @@ func TestStudentModel(t *testing.T) {
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = mockStudent(func(mock sqlmock.Sqlmock) {
|
||||
err = mockStudent(t, func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectExec(fmt.Sprintf("update %s", testTable)).WithArgs(data.Class, testUpdateName, data.Age, data.Score, testInsertId).WillReturnResult(sqlmock.NewResult(testInsertId, testRowsAffected))
|
||||
}, func(m StudentModel, redis *redis.Redis) {
|
||||
data.Name = testUpdateName
|
||||
@@ -93,7 +93,7 @@ func TestStudentModel(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
|
||||
data.Name = testUpdateName
|
||||
err = mockStudent(func(mock sqlmock.Sqlmock) {
|
||||
err = mockStudent(t, func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectQuery(fmt.Sprintf("select (.+) from %s ", testTable)).
|
||||
WithArgs(testInsertId).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "class", "name", "age", "score", "create_time", "update_time"}).AddRow(testInsertId, data.Class, data.Name, data.Age, data.Score, testTimeValue, testTimeValue))
|
||||
@@ -111,7 +111,7 @@ func TestStudentModel(t *testing.T) {
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = mockStudent(func(mock sqlmock.Sqlmock) {
|
||||
err = mockStudent(t, func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectQuery(fmt.Sprintf("select (.+) from %s ", testTable)).
|
||||
WithArgs(class, testUpdateName).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "class", "name", "age", "score", "create_time", "update_time"}).AddRow(testInsertId, data.Class, data.Name, data.Age, data.Score, testTimeValue, testTimeValue))
|
||||
@@ -126,7 +126,7 @@ func TestStudentModel(t *testing.T) {
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = mockStudent(func(mock sqlmock.Sqlmock) {
|
||||
err = mockStudent(t, func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectExec(fmt.Sprintf("delete from %s where `id` = ?", testTable)).WithArgs(testInsertId).WillReturnResult(sqlmock.NewResult(testInsertId, testRowsAffected))
|
||||
}, func(m StudentModel, redis *redis.Redis) {
|
||||
err = m.Delete(testInsertId, class, testUpdateName)
|
||||
@@ -228,7 +228,7 @@ func TestUserModel(t *testing.T) {
|
||||
}
|
||||
|
||||
// with cache
|
||||
func mockStudent(mockFn func(mock sqlmock.Sqlmock), fn func(m StudentModel, r *redis.Redis)) error {
|
||||
func mockStudent(t *testing.T, mockFn func(mock sqlmock.Sqlmock), fn func(m StudentModel, r *redis.Redis)) error {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -241,13 +241,7 @@ func mockStudent(mockFn func(mock sqlmock.Sqlmock), fn func(m StudentModel, r *r
|
||||
mock.ExpectCommit()
|
||||
|
||||
conn := mocksql.NewMockConn(db)
|
||||
r, clean, err := redistest.CreateRedis()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer clean()
|
||||
|
||||
r := redistest.CreateRedis(t)
|
||||
m := NewStudentModel(conn, cache.CacheConf{
|
||||
{
|
||||
RedisConf: redis.RedisConf{
|
||||
|
||||
@@ -128,7 +128,7 @@ func unmarshalRow(v any, scanner rowsScanner, strict bool) error {
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
if err := mapping.ValidatePtr(&rv); err != nil {
|
||||
if err := mapping.ValidatePtr(rv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ func unmarshalRow(v any, scanner rowsScanner, strict bool) error {
|
||||
|
||||
func unmarshalRows(v any, scanner rowsScanner, strict bool) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if err := mapping.ValidatePtr(&rv); err != nil {
|
||||
if err := mapping.ValidatePtr(rv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
9
tools/goctl/pkg/env/env.go
vendored
9
tools/goctl/pkg/env/env.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||
sortedmap "github.com/zeromicro/go-zero/tools/goctl/pkg/collection"
|
||||
@@ -111,6 +112,14 @@ func Get(key string) string {
|
||||
return GetOr(key, "")
|
||||
}
|
||||
|
||||
// Set sets the environment variable for testing
|
||||
func Set(t *testing.T, key, value string) {
|
||||
goctlEnv.SetKV(key, value)
|
||||
t.Cleanup(func() {
|
||||
goctlEnv.Remove(key)
|
||||
})
|
||||
}
|
||||
|
||||
func GetOr(key, def string) string {
|
||||
return goctlEnv.GetStringOr(key, def)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
@@ -18,13 +19,14 @@ type Analyzer struct {
|
||||
|
||||
func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
|
||||
isLiteralType := func(dt ast.DataType) bool {
|
||||
_, ok := dt.(*ast.BaseDataType)
|
||||
if ok {
|
||||
if _, ok := dt.(*ast.BaseDataType); ok {
|
||||
return true
|
||||
}
|
||||
_, ok = dt.(*ast.AnyDataType)
|
||||
|
||||
_, ok := dt.(*ast.AnyDataType)
|
||||
return ok
|
||||
}
|
||||
|
||||
switch v := (in).(type) {
|
||||
case *ast.BaseDataType:
|
||||
raw := v.RawText()
|
||||
@@ -33,6 +35,7 @@ func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
|
||||
RawName: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return spec.DefineStruct{RawName: raw}, nil
|
||||
case *ast.AnyDataType:
|
||||
return nil, ast.SyntaxError(v.Pos(), "unsupported any type")
|
||||
@@ -47,10 +50,12 @@ func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
|
||||
if !v.Key.CanEqual() {
|
||||
return nil, ast.SyntaxError(v.Pos(), "map key <%T> must be equal data type", v)
|
||||
}
|
||||
|
||||
value, err := a.astTypeToSpec(v.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spec.MapType{
|
||||
RawName: v.RawText(),
|
||||
Key: v.RawText(),
|
||||
@@ -66,6 +71,7 @@ func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spec.PointerType{
|
||||
RawName: v.RawText(),
|
||||
Type: value,
|
||||
@@ -74,10 +80,12 @@ func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
|
||||
if v.Length.Token.Type == token.ELLIPSIS {
|
||||
return nil, ast.SyntaxError(v.Pos(), "Array: unsupported dynamic length")
|
||||
}
|
||||
|
||||
value, err := a.astTypeToSpec(v.DataType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spec.ArrayType{
|
||||
RawName: v.RawText(),
|
||||
Value: value,
|
||||
@@ -87,6 +95,7 @@ func (a *Analyzer) astTypeToSpec(in ast.DataType) (spec.Type, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spec.ArrayType{
|
||||
RawName: v.RawText(),
|
||||
Value: value,
|
||||
@@ -101,7 +110,27 @@ func (a *Analyzer) convert2Spec() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.fillService()
|
||||
if err := a.fillService(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.SliceStable(a.spec.Types, func(i, j int) bool {
|
||||
return a.spec.Types[i].Name() < a.spec.Types[j].Name()
|
||||
})
|
||||
|
||||
groups := make([]spec.Group, 0, len(a.spec.Service.Groups))
|
||||
for _, v := range a.spec.Service.Groups {
|
||||
sort.SliceStable(v.Routes, func(i, j int) bool {
|
||||
return v.Routes[i].Path < v.Routes[j].Path
|
||||
})
|
||||
groups = append(groups, v)
|
||||
}
|
||||
sort.SliceStable(groups, func(i, j int) bool {
|
||||
return groups[i].Annotation.Properties["group"] < groups[j].Annotation.Properties["group"]
|
||||
})
|
||||
a.spec.Service.Groups = groups
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Analyzer) convertAtDoc(atDoc ast.AtDocStmt) spec.AtDoc {
|
||||
@@ -146,6 +175,7 @@ func (a *Analyzer) fieldToMember(field *ast.ElemExpr) (spec.Member, error) {
|
||||
if field.Tag != nil {
|
||||
m.Tag = field.Tag.Token.Text
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -242,8 +272,7 @@ func (a *Analyzer) fillTypes() error {
|
||||
for _, item := range a.api.TypeStmt {
|
||||
switch v := (item).(type) {
|
||||
case *ast.TypeLiteralStmt:
|
||||
err := a.fillTypeExpr(v.Expr)
|
||||
if err != nil {
|
||||
if err := a.fillTypeExpr(v.Expr); err != nil {
|
||||
return err
|
||||
}
|
||||
case *ast.TypeGroupStmt:
|
||||
|
||||
@@ -31,18 +31,40 @@ func convert2API(a *ast.AST, importManager map[string]placeholder.Type) (*API, e
|
||||
one := a.Stmts[0]
|
||||
syntax, ok := one.(*ast.SyntaxStmt)
|
||||
if !ok {
|
||||
return nil, ast.SyntaxError(one.Pos(), "expected syntax statement, got <%T>", one)
|
||||
syntax = &ast.SyntaxStmt{
|
||||
Syntax: ast.NewTokenNode(token.Token{
|
||||
Type: token.IDENT,
|
||||
Text: token.Syntax,
|
||||
}),
|
||||
Assign: ast.NewTokenNode(token.Token{
|
||||
Type: token.ASSIGN,
|
||||
Text: "=",
|
||||
}),
|
||||
Value: ast.NewTokenNode(token.Token{
|
||||
Type: token.STRING,
|
||||
Text: `"v1"`,
|
||||
}),
|
||||
}
|
||||
}
|
||||
api.Syntax = syntax
|
||||
|
||||
for i := 1; i < len(a.Stmts); i++ {
|
||||
api.Syntax = syntax
|
||||
var hasSyntax, hasInfo bool
|
||||
for i := 0; i < len(a.Stmts); i++ {
|
||||
one := a.Stmts[i]
|
||||
switch val := one.(type) {
|
||||
case *ast.SyntaxStmt:
|
||||
return nil, ast.DuplicateStmtError(val.Pos(), "duplicate syntax statement")
|
||||
if hasSyntax {
|
||||
return nil, ast.DuplicateStmtError(val.Pos(), "duplicate syntax statement")
|
||||
} else {
|
||||
hasSyntax = true
|
||||
}
|
||||
case *ast.InfoStmt:
|
||||
if api.info != nil {
|
||||
return nil, ast.DuplicateStmtError(val.Pos(), "duplicate info statement")
|
||||
if hasInfo {
|
||||
return nil, ast.DuplicateStmtError(val.Pos(), "duplicate info statement")
|
||||
}
|
||||
} else {
|
||||
hasInfo = true
|
||||
}
|
||||
api.info = val
|
||||
case ast.ImportStmt:
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// test case: expected syntax statement
|
||||
info ()
|
||||
|
||||
-----
|
||||
// test case: duplicate syntax statement
|
||||
syntax = "v1"
|
||||
syntax = "v1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.18
|
||||
FROM golang:1.19
|
||||
|
||||
ENV TZ Asia/Shanghai
|
||||
ENV GOPROXY https://goproxy.cn,direct
|
||||
|
||||
@@ -81,13 +81,13 @@ func Test_getRealModule(t *testing.T) {
|
||||
"Path":"foo",
|
||||
"Dir":"/home/foo",
|
||||
"GoMod":"/home/foo/go.mod",
|
||||
"GoVersion":"go1.18"
|
||||
"GoVersion":"go1.19"
|
||||
}
|
||||
{
|
||||
"Path":"bar",
|
||||
"Dir":"/home/bar",
|
||||
"GoMod":"/home/bar/go.mod",
|
||||
"GoVersion":"go1.18"
|
||||
"GoVersion":"go1.19"
|
||||
}`, nil
|
||||
},
|
||||
},
|
||||
@@ -95,7 +95,7 @@ func Test_getRealModule(t *testing.T) {
|
||||
Path: "bar",
|
||||
Dir: "/home/bar",
|
||||
GoMod: "/home/bar/go.mod",
|
||||
GoVersion: "go1.18",
|
||||
GoVersion: "go1.19",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -143,26 +143,26 @@ func TestDecodePackages(t *testing.T) {
|
||||
"Path":"foo",
|
||||
"Dir":"/home/foo",
|
||||
"GoMod":"/home/foo/go.mod",
|
||||
"GoVersion":"go1.18"
|
||||
"GoVersion":"go1.19"
|
||||
}
|
||||
{
|
||||
"Path":"bar",
|
||||
"Dir":"/home/bar",
|
||||
"GoMod":"/home/bar/go.mod",
|
||||
"GoVersion":"go1.18"
|
||||
"GoVersion":"go1.19"
|
||||
}`),
|
||||
want: []Module{
|
||||
{
|
||||
Path: "foo",
|
||||
Dir: "/home/foo",
|
||||
GoMod: "/home/foo/go.mod",
|
||||
GoVersion: "go1.18",
|
||||
GoVersion: "go1.19",
|
||||
},
|
||||
{
|
||||
Path: "bar",
|
||||
Dir: "/home/bar",
|
||||
GoMod: "/home/bar/go.mod",
|
||||
GoVersion: "go1.18",
|
||||
GoVersion: "go1.19",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -173,14 +173,14 @@ func TestDecodePackages(t *testing.T) {
|
||||
"Path":"foo",
|
||||
"Dir":"/home/foo",
|
||||
"GoMod":"/home/foo/go.mod",
|
||||
"GoVersion":"go1.18"
|
||||
"GoVersion":"go1.19"
|
||||
}`),
|
||||
want: []Module{
|
||||
{
|
||||
Path: "foo",
|
||||
Dir: "/home/foo",
|
||||
GoMod: "/home/foo/go.mod",
|
||||
GoVersion: "go1.18",
|
||||
GoVersion: "go1.19",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,3 +110,8 @@ func DontLogClientContentForMethod(method string) {
|
||||
func SetClientSlowThreshold(threshold time.Duration) {
|
||||
clientinterceptors.SetSlowThreshold(threshold)
|
||||
}
|
||||
|
||||
// WithCallTimeout return a call option with given timeout to make a method call.
|
||||
func WithCallTimeout(timeout time.Duration) grpc.CallOption {
|
||||
return clientinterceptors.WithCallTimeout(timeout)
|
||||
}
|
||||
|
||||
@@ -43,30 +43,35 @@ func TestDepositServer_Deposit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
amount float32
|
||||
timeout time.Duration
|
||||
res *mock.DepositResponse
|
||||
errCode codes.Code
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
"invalid request with negative amount",
|
||||
-1.11,
|
||||
nil,
|
||||
codes.InvalidArgument,
|
||||
fmt.Sprintf("cannot deposit %v", -1.11),
|
||||
name: "invalid request with negative amount",
|
||||
amount: -1.11,
|
||||
errCode: codes.InvalidArgument,
|
||||
errMsg: fmt.Sprintf("cannot deposit %v", -1.11),
|
||||
},
|
||||
{
|
||||
"valid request with non negative amount",
|
||||
0.00,
|
||||
&mock.DepositResponse{Ok: true},
|
||||
codes.OK,
|
||||
"",
|
||||
name: "valid request with non negative amount",
|
||||
res: &mock.DepositResponse{Ok: true},
|
||||
errCode: codes.OK,
|
||||
},
|
||||
{
|
||||
"valid request with long handling time",
|
||||
2000.00,
|
||||
nil,
|
||||
codes.DeadlineExceeded,
|
||||
"context deadline exceeded",
|
||||
name: "valid request with long handling time",
|
||||
amount: 2000.00,
|
||||
errCode: codes.DeadlineExceeded,
|
||||
errMsg: "context deadline exceeded",
|
||||
},
|
||||
{
|
||||
name: "valid request with timeout call option",
|
||||
amount: 2000.00,
|
||||
timeout: time.Second * 3,
|
||||
res: &mock.DepositResponse{Ok: true},
|
||||
errCode: codes.OK,
|
||||
errMsg: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -156,9 +161,22 @@ func TestDepositServer_Deposit(t *testing.T) {
|
||||
client := client
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cli := mock.NewDepositServiceClient(client.Conn())
|
||||
request := &mock.DepositRequest{Amount: tt.amount}
|
||||
response, err := cli.Deposit(context.Background(), request)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
response *mock.DepositResponse
|
||||
err error
|
||||
)
|
||||
|
||||
if tt.timeout > 0 {
|
||||
response, err = cli.Deposit(ctx, request, WithCallTimeout(tt.timeout))
|
||||
} else {
|
||||
response, err = cli.Deposit(ctx, request)
|
||||
}
|
||||
|
||||
if response != nil {
|
||||
assert.True(t, len(response.String()) > 0)
|
||||
if response.GetOk() != tt.res.GetOk() {
|
||||
|
||||
@@ -17,6 +17,8 @@ type (
|
||||
ServerMiddlewaresConf = internal.ServerMiddlewaresConf
|
||||
// StatConf defines the stat config.
|
||||
StatConf = internal.StatConf
|
||||
// MethodTimeoutConf defines specified timeout for gRPC method.
|
||||
MethodTimeoutConf = internal.MethodTimeoutConf
|
||||
|
||||
// A RpcClientConf is a rpc client config.
|
||||
RpcClientConf struct {
|
||||
@@ -45,6 +47,8 @@ type (
|
||||
// grpc health check switch
|
||||
Health bool `json:",default=true"`
|
||||
Middlewares ServerMiddlewaresConf
|
||||
// setting specified timeout for gRPC method
|
||||
MethodTimeouts []MethodTimeoutConf `json:",optional"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ var (
|
||||
Name: "duration_ms",
|
||||
Help: "rpc client requests duration(ms).",
|
||||
Labels: []string{"method"},
|
||||
Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000},
|
||||
Buckets: []float64{1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2000, 5000},
|
||||
})
|
||||
|
||||
metricClientReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||
|
||||
@@ -7,17 +7,41 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// TimeoutCallOption is a call option that controls timeout.
|
||||
type TimeoutCallOption struct {
|
||||
grpc.EmptyCallOption
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// TimeoutInterceptor is an interceptor that controls timeout.
|
||||
func TimeoutInterceptor(timeout time.Duration) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn,
|
||||
invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
if timeout <= 0 {
|
||||
t := getTimeoutFromCallOptions(opts, timeout)
|
||||
if t <= 0 {
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, t)
|
||||
defer cancel()
|
||||
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCallTimeout returns a call option that controls method call timeout.
|
||||
func WithCallTimeout(timeout time.Duration) grpc.CallOption {
|
||||
return TimeoutCallOption{
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeoutFromCallOptions(opts []grpc.CallOption, defaultTimeout time.Duration) time.Duration {
|
||||
for _, opt := range opts {
|
||||
if o, ok := opt.(TimeoutCallOption); ok {
|
||||
return o.timeout
|
||||
}
|
||||
}
|
||||
|
||||
return defaultTimeout
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user