mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-11 08:50:00 +08:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c4908342 | ||
|
|
48625fa381 | ||
|
|
83a776a190 | ||
|
|
431f9af43e | ||
|
|
8c2f4c1899 | ||
|
|
919477ffe4 | ||
|
|
400386459c | ||
|
|
ebe0801d2f | ||
|
|
b76d85f204 | ||
|
|
28ba57afb3 | ||
|
|
d6873047ce | ||
|
|
54c0f2e5cf | ||
|
|
7795231cc6 | ||
|
|
4835e4fe51 | ||
|
|
daef970091 | ||
|
|
05020a92e8 | ||
|
|
a1bbac3c6c | ||
|
|
22c98beb24 | ||
|
|
8fd710d5e7 | ||
|
|
91a735ae47 | ||
|
|
39c662eece | ||
|
|
5e63002cf8 | ||
|
|
c46bcf7e1b | ||
|
|
3c65bdbb66 | ||
|
|
5630bce286 | ||
|
|
75524da21e | ||
|
|
ede7e683fd | ||
|
|
eb14d1347e | ||
|
|
c220b5d886 | ||
|
|
5e8e21b257 | ||
|
|
0635a4ac96 | ||
|
|
c71b753c78 | ||
|
|
2f8cffc699 | ||
|
|
9c1aa6da3d | ||
|
|
da67ea2300 | ||
|
|
72dd2736f5 | ||
|
|
24695bba09 | ||
|
|
c7c43062c5 | ||
|
|
97e1ea0633 | ||
|
|
04b9737a61 | ||
|
|
b0fb246693 | ||
|
|
41140ac78c | ||
|
|
1281904572 | ||
|
|
c8a8ff7cad | ||
|
|
df2799fff1 | ||
|
|
fd8ee0b851 | ||
|
|
6ecc5e7b73 | ||
|
|
52963c2ebf | ||
|
|
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 | ||
|
|
4878f90546 | ||
|
|
421e6617b1 | ||
|
|
0ee7a271d3 | ||
|
|
af022b9655 | ||
|
|
98d46261d9 | ||
|
|
4222fd97bc | ||
|
|
814852f0b8 | ||
|
|
ded2888759 | ||
|
|
18d66a795d | ||
|
|
4211672bfd | ||
|
|
68df0c3620 | ||
|
|
5e435b6a76 | ||
|
|
0dcede6457 | ||
|
|
cc21f5fae2 | ||
|
|
b22ad50d59 | ||
|
|
974252980c | ||
|
|
8d83986d27 | ||
|
|
6821b0a7dd | ||
|
|
1ba1724c65 | ||
|
|
ca5a7df5b0 | ||
|
|
69a3024853 | ||
|
|
fd3abf3717 | ||
|
|
99b3750d10 | ||
|
|
33f6d7ebb8 | ||
|
|
c4ef9ceb68 | ||
|
|
e95861f28a | ||
|
|
d3cd7b17c0 | ||
|
|
a50515496c | ||
|
|
0423313d9b | ||
|
|
7bbe7de05f | ||
|
|
83a451f2f4 | ||
|
|
d2a874f21d | ||
|
|
fd85b24b25 | ||
|
|
14fcbd7658 | ||
|
|
cb3ffc76a3 | ||
|
|
45fbd7dc35 | ||
|
|
af821cf794 | ||
|
|
ec69950153 | ||
|
|
ce5e78db53 | ||
|
|
ed75802eaa | ||
|
|
76c92b571d | ||
|
|
a2e703c53e | ||
|
|
ca698deb2a | ||
|
|
a9f4aab86b | ||
|
|
c3f57e9b0a | ||
|
|
ad4cce959d | ||
|
|
279123f4a7 | ||
|
|
457eb1961b | ||
|
|
63df384a4b | ||
|
|
42bfa26e2b | ||
|
|
ff04356704 | ||
|
|
05db706c62 | ||
|
|
ef2e0d859d | ||
|
|
05ec16ae9d | ||
|
|
13e685e0db | ||
|
|
c10f44b74e | ||
|
|
57644420ed |
@@ -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. 🔥
|
||||
@@ -1,76 +1,127 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -25,8 +25,14 @@ func (r *Ring) Add(v any) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
r.elements[r.index%len(r.elements)] = v
|
||||
rlen := len(r.elements)
|
||||
r.elements[r.index%rlen] = v
|
||||
r.index++
|
||||
|
||||
// prevent ring index overflow
|
||||
if r.index >= rlen<<1 {
|
||||
r.index -= rlen
|
||||
}
|
||||
}
|
||||
|
||||
// Take takes all items from r.
|
||||
@@ -36,16 +42,18 @@ func (r *Ring) Take() []any {
|
||||
|
||||
var size int
|
||||
var start int
|
||||
if r.index > len(r.elements) {
|
||||
size = len(r.elements)
|
||||
start = r.index % len(r.elements)
|
||||
rlen := len(r.elements)
|
||||
|
||||
if r.index > rlen {
|
||||
size = rlen
|
||||
start = r.index % rlen
|
||||
} else {
|
||||
size = r.index
|
||||
}
|
||||
|
||||
elements := make([]any, size)
|
||||
for i := 0; i < size; i++ {
|
||||
elements[i] = r.elements[(start+i)%len(r.elements)]
|
||||
elements[i] = r.elements[(start+i)%rlen]
|
||||
}
|
||||
|
||||
return elements
|
||||
|
||||
@@ -29,6 +29,8 @@ func NewSafeMap() *SafeMap {
|
||||
// Del deletes the value with the given key from m.
|
||||
func (m *SafeMap) Del(key any) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if _, ok := m.dirtyOld[key]; ok {
|
||||
delete(m.dirtyOld, key)
|
||||
m.deletionOld++
|
||||
@@ -52,7 +54,6 @@ func (m *SafeMap) Del(key any) {
|
||||
m.dirtyNew = make(map[any]any)
|
||||
m.deletionNew = 0
|
||||
}
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
// Get gets the value with the given key from m.
|
||||
@@ -89,6 +90,8 @@ func (m *SafeMap) Range(f func(key, val any) bool) {
|
||||
// Set sets the value into m with the given key.
|
||||
func (m *SafeMap) Set(key, value any) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.deletionOld <= maxDeletion {
|
||||
if _, ok := m.dirtyNew[key]; ok {
|
||||
delete(m.dirtyNew, key)
|
||||
@@ -102,7 +105,6 @@ func (m *SafeMap) Set(key, value any) {
|
||||
}
|
||||
m.dirtyNew[key] = value
|
||||
}
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
// Size returns the size of m.
|
||||
|
||||
@@ -147,3 +147,65 @@ func TestSafeMap_Range(t *testing.T) {
|
||||
assert.Equal(t, m.dirtyNew, newMap.dirtyNew)
|
||||
assert.Equal(t, m.dirtyOld, newMap.dirtyOld)
|
||||
}
|
||||
|
||||
func TestSetManyTimes(t *testing.T) {
|
||||
const iteration = maxDeletion * 2
|
||||
m := NewSafeMap()
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
var count int
|
||||
m.Range(func(k, v any) bool {
|
||||
count++
|
||||
return count < maxDeletion/2
|
||||
})
|
||||
assert.Equal(t, maxDeletion/2, count)
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
for i := 0; i < iteration; i++ {
|
||||
m.Set(i, i)
|
||||
if i%3 == 0 {
|
||||
m.Del(i / 2)
|
||||
}
|
||||
}
|
||||
|
||||
count = 0
|
||||
m.Range(func(k, v any) bool {
|
||||
count++
|
||||
return count < maxDeletion
|
||||
})
|
||||
assert.Equal(t, maxDeletion, count)
|
||||
}
|
||||
|
||||
func TestSetManyTimesNew(t *testing.T) {
|
||||
m := NewSafeMap()
|
||||
for i := 0; i < maxDeletion*3; i++ {
|
||||
m.Set(i, i)
|
||||
}
|
||||
for i := 0; i < maxDeletion*2; i++ {
|
||||
m.Del(i)
|
||||
}
|
||||
for i := 0; i < maxDeletion*3; i++ {
|
||||
m.Set(i+maxDeletion*3, i+maxDeletion*3)
|
||||
}
|
||||
for i := 0; i < maxDeletion*2; i++ {
|
||||
m.Del(i + maxDeletion*2)
|
||||
}
|
||||
for i := 0; i < maxDeletion-copyThreshold+1; i++ {
|
||||
m.Del(i + maxDeletion*2)
|
||||
}
|
||||
assert.Equal(t, 0, len(m.dirtyNew))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"github.com/zeromicro/go-zero/core/threading"
|
||||
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
@@ -22,6 +24,7 @@ var (
|
||||
clusters: make(map[string]*cluster),
|
||||
}
|
||||
connManager = syncx.NewResourceManager()
|
||||
errClosed = errors.New("etcd monitor chan has been closed")
|
||||
)
|
||||
|
||||
// A Registry is a registry that manages the etcd client connections.
|
||||
@@ -288,40 +291,47 @@ func (c *cluster) reload(cli EtcdClient) {
|
||||
|
||||
func (c *cluster) watch(cli EtcdClient, key string, rev int64) {
|
||||
for {
|
||||
if c.watchStream(cli, key, rev) {
|
||||
err := c.watchStream(cli, key, rev)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if rev != 0 && errors.Is(err, rpctypes.ErrCompacted) {
|
||||
logx.Errorf("etcd watch stream has been compacted, try to reload, rev %d", rev)
|
||||
rev = c.load(cli, key)
|
||||
}
|
||||
|
||||
// log the error and retry
|
||||
logx.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) bool {
|
||||
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) error {
|
||||
var rch clientv3.WatchChan
|
||||
if rev != 0 {
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix(),
|
||||
clientv3.WithRev(rev+1))
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key),
|
||||
clientv3.WithPrefix(), clientv3.WithRev(rev+1))
|
||||
} else {
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key),
|
||||
clientv3.WithPrefix())
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case wresp, ok := <-rch:
|
||||
if !ok {
|
||||
logx.Error("etcd monitor chan has been closed")
|
||||
return false
|
||||
return errClosed
|
||||
}
|
||||
if wresp.Canceled {
|
||||
logx.Errorf("etcd monitor chan has been canceled, error: %v", wresp.Err())
|
||||
return false
|
||||
return fmt.Errorf("etcd monitor chan has been canceled, error: %w", wresp.Err())
|
||||
}
|
||||
if wresp.Err() != nil {
|
||||
logx.Error(fmt.Sprintf("etcd monitor chan error: %v", wresp.Err()))
|
||||
return false
|
||||
return fmt.Errorf("etcd monitor chan error: %w", wresp.Err())
|
||||
}
|
||||
|
||||
c.handleWatchEvents(key, wresp.Events)
|
||||
case <-c.done:
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ func (bp *BufferPool) Get() *bytes.Buffer {
|
||||
|
||||
// Put returns buf into bp.
|
||||
func (bp *BufferPool) Put(buf *bytes.Buffer) {
|
||||
if buf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if buf.Cap() < bp.capability {
|
||||
bp.pool.Put(buf)
|
||||
}
|
||||
|
||||
@@ -13,3 +13,26 @@ func TestBufferPool(t *testing.T) {
|
||||
pool.Put(bytes.NewBuffer(make([]byte, 0, 2*capacity)))
|
||||
assert.True(t, pool.Get().Cap() <= capacity)
|
||||
}
|
||||
|
||||
func TestBufferPool_Put(t *testing.T) {
|
||||
t.Run("with nil buf", func(t *testing.T) {
|
||||
pool := NewBufferPool(1024)
|
||||
pool.Put(nil)
|
||||
val := pool.Get()
|
||||
assert.IsType(t, new(bytes.Buffer), val)
|
||||
})
|
||||
|
||||
t.Run("with less-cap buf", func(t *testing.T) {
|
||||
pool := NewBufferPool(1024)
|
||||
pool.Put(bytes.NewBuffer(make([]byte, 0, 512)))
|
||||
val := pool.Get()
|
||||
assert.IsType(t, new(bytes.Buffer), val)
|
||||
})
|
||||
|
||||
t.Run("with more-cap buf", func(t *testing.T) {
|
||||
pool := NewBufferPool(1024)
|
||||
pool.Put(bytes.NewBuffer(make([]byte, 0, 1024<<1)))
|
||||
val := pool.Get()
|
||||
assert.IsType(t, new(bytes.Buffer), val)
|
||||
})
|
||||
}
|
||||
|
||||
12
core/iox/nopcloser_test.go
Normal file
12
core/iox/nopcloser_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNopCloser(t *testing.T) {
|
||||
closer := NopCloser(nil)
|
||||
assert.NoError(t, closer.Close())
|
||||
}
|
||||
@@ -35,6 +35,16 @@ func KeepSpace() TextReadOption {
|
||||
}
|
||||
}
|
||||
|
||||
// LimitDupReadCloser returns two io.ReadCloser that read from the first will be written to the second.
|
||||
// But the second io.ReadCloser is limited to up to n bytes.
|
||||
// The first returned reader needs to be read first, because the content
|
||||
// read from it will be written to the underlying buffer of the second reader.
|
||||
func LimitDupReadCloser(reader io.ReadCloser, n int64) (io.ReadCloser, io.ReadCloser) {
|
||||
var buf bytes.Buffer
|
||||
tee := LimitTeeReader(reader, &buf, n)
|
||||
return io.NopCloser(tee), io.NopCloser(&buf)
|
||||
}
|
||||
|
||||
// ReadBytes reads exactly the bytes with the length of len(buf)
|
||||
func ReadBytes(reader io.Reader, buf []byte) error {
|
||||
var got int
|
||||
|
||||
@@ -51,6 +51,11 @@ b`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadTextError(t *testing.T) {
|
||||
_, err := ReadText("not-exist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestReadTextLines(t *testing.T) {
|
||||
text := `1
|
||||
|
||||
@@ -94,6 +99,11 @@ func TestReadTextLines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadTextLinesError(t *testing.T) {
|
||||
_, err := ReadTextLines("not-exist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestDupReadCloser(t *testing.T) {
|
||||
input := "hello"
|
||||
reader := io.NopCloser(bytes.NewBufferString(input))
|
||||
@@ -108,6 +118,29 @@ func TestDupReadCloser(t *testing.T) {
|
||||
verify(r2)
|
||||
}
|
||||
|
||||
func TestLimitDupReadCloser(t *testing.T) {
|
||||
input := "hello world"
|
||||
limitBytes := int64(4)
|
||||
reader := io.NopCloser(bytes.NewBufferString(input))
|
||||
r1, r2 := LimitDupReadCloser(reader, limitBytes)
|
||||
verify := func(r io.Reader) {
|
||||
output, err := io.ReadAll(r)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, input, string(output))
|
||||
}
|
||||
verifyLimit := func(r io.Reader, limit int64) {
|
||||
output, err := io.ReadAll(r)
|
||||
if limit < int64(len(input)) {
|
||||
input = input[:limit]
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, input, string(output))
|
||||
}
|
||||
|
||||
verify(r1)
|
||||
verifyLimit(r2, limitBytes)
|
||||
}
|
||||
|
||||
func TestReadBytes(t *testing.T) {
|
||||
reader := io.NopCloser(bytes.NewBufferString("helloworld"))
|
||||
buf := make([]byte, 5)
|
||||
|
||||
35
core/iox/tee.go
Normal file
35
core/iox/tee.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package iox
|
||||
|
||||
import "io"
|
||||
|
||||
// LimitTeeReader returns a Reader that writes up to n bytes to w what it reads from r.
|
||||
// First n bytes reads from r performed through it are matched with
|
||||
// corresponding writes to w. There is no internal buffering -
|
||||
// the write must complete before the first n bytes read completes.
|
||||
// Any error encountered while writing is reported as a read error.
|
||||
func LimitTeeReader(r io.Reader, w io.Writer, n int64) io.Reader {
|
||||
return &limitTeeReader{r, w, n}
|
||||
}
|
||||
|
||||
type limitTeeReader struct {
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
n int64 // limit bytes remaining
|
||||
}
|
||||
|
||||
func (t *limitTeeReader) Read(p []byte) (n int, err error) {
|
||||
n, err = t.r.Read(p)
|
||||
if n > 0 && t.n > 0 {
|
||||
limit := int64(n)
|
||||
if limit > t.n {
|
||||
limit = t.n
|
||||
}
|
||||
if n, err := t.w.Write(p[:limit]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
t.n -= limit
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
40
core/iox/tee_test.go
Normal file
40
core/iox/tee_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package iox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLimitTeeReader(t *testing.T) {
|
||||
limit := int64(4)
|
||||
src := []byte("hello, world")
|
||||
dst := make([]byte, len(src))
|
||||
rb := bytes.NewBuffer(src)
|
||||
wb := new(bytes.Buffer)
|
||||
r := LimitTeeReader(rb, wb, limit)
|
||||
if n, err := io.ReadFull(r, dst); err != nil || n != len(src) {
|
||||
t.Fatalf("ReadFull(r, dst) = %d, %v; want %d, nil", n, err, len(src))
|
||||
}
|
||||
if !bytes.Equal(dst, src) {
|
||||
t.Errorf("bytes read = %q want %q", dst, src)
|
||||
}
|
||||
if !bytes.Equal(wb.Bytes(), src[:limit]) {
|
||||
t.Errorf("bytes written = %q want %q", wb.Bytes(), src)
|
||||
}
|
||||
|
||||
n, err := r.Read(dst)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
|
||||
rb = bytes.NewBuffer(src)
|
||||
pr, pw := io.Pipe()
|
||||
if assert.NoError(t, pr.Close()) {
|
||||
r = LimitTeeReader(rb, pw, limit)
|
||||
n, err := io.ReadFull(r, dst)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Equal(t, io.ErrClosedPipe, err)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package iox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
@@ -26,7 +27,7 @@ func CountLines(file string) (int, error) {
|
||||
count += bytes.Count(buf[:c], lineSep)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
case errors.Is(err, io.EOF):
|
||||
if noEol {
|
||||
count++
|
||||
}
|
||||
|
||||
@@ -24,3 +24,8 @@ func TestCountLines(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 4, lines)
|
||||
}
|
||||
|
||||
func TestCountLinesError(t *testing.T) {
|
||||
_, err := CountLines("not-exist")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package iox
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -22,3 +23,10 @@ func TestScanner(t *testing.T) {
|
||||
}
|
||||
assert.EqualValues(t, []string{"1", "2", "3", "4"}, lines)
|
||||
}
|
||||
|
||||
func TestBadScanner(t *testing.T) {
|
||||
scanner := NewTextLineScanner(iotest.ErrReader(iotest.ErrTimeout))
|
||||
assert.False(t, scanner.Scan())
|
||||
_, err := scanner.Line()
|
||||
assert.ErrorIs(t, err, iotest.ErrTimeout)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -298,6 +298,7 @@ func (l *RotateLogger) initialize() error {
|
||||
if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.currentSize = fileInfo.Size()
|
||||
}
|
||||
|
||||
@@ -381,7 +382,15 @@ func (l *RotateLogger) startWorker() {
|
||||
case event := <-l.channel:
|
||||
l.write(event)
|
||||
case <-l.done:
|
||||
return
|
||||
// avoid losing logs before closing.
|
||||
for {
|
||||
select {
|
||||
case event := <-l.channel:
|
||||
l.write(event)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -206,6 +206,27 @@ func TestRotateLoggerClose(t *testing.T) {
|
||||
_, err := logger.Write([]byte("foo"))
|
||||
assert.ErrorIs(t, err, ErrLogFileClosed)
|
||||
})
|
||||
|
||||
t.Run("close without losing logs", func(t *testing.T) {
|
||||
text := "foo"
|
||||
filename, err := fs.TempFilenameWithText(text)
|
||||
assert.Nil(t, err)
|
||||
if len(filename) > 0 {
|
||||
defer os.Remove(filename)
|
||||
}
|
||||
logger, err := NewLogger(filename, new(DailyRotateRule), false)
|
||||
assert.Nil(t, err)
|
||||
msg := []byte("foo")
|
||||
n := 100
|
||||
for i := 0; i < n; i++ {
|
||||
_, err = logger.Write(msg)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.Nil(t, logger.Close())
|
||||
bs, err := os.ReadFile(filename)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(msg)*n+len(text), len(bs))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotateLoggerGetBackupFilename(t *testing.T) {
|
||||
@@ -496,6 +517,21 @@ func TestGzipFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotateLogger_WithExistingFile(t *testing.T) {
|
||||
const body = "foo"
|
||||
filename, err := fs.TempFilenameWithText(body)
|
||||
assert.Nil(t, err)
|
||||
if len(filename) > 0 {
|
||||
defer os.Remove(filename)
|
||||
}
|
||||
|
||||
rule := NewSizeLimitRotateRule(filename, "-", 1, 100, 3, false)
|
||||
logger, err := NewLogger(filename, rule, false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(len(body)), logger.currentSize)
|
||||
assert.Nil(t, logger.Close())
|
||||
}
|
||||
|
||||
func BenchmarkRotateLogger(b *testing.B) {
|
||||
filename := "./test.log"
|
||||
filename2 := "./test2.log"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -18,8 +19,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultKeyName = "key"
|
||||
delimiter = '.'
|
||||
defaultKeyName = "key"
|
||||
delimiter = '.'
|
||||
ignoreKey = "-"
|
||||
numberTypeString = "number"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,6 +52,7 @@ type (
|
||||
unmarshalOptions struct {
|
||||
fillDefault bool
|
||||
fromString bool
|
||||
opaqueKeys bool
|
||||
canonicalKey func(key string) string
|
||||
}
|
||||
)
|
||||
@@ -72,7 +76,11 @@ func UnmarshalKey(m map[string]any, v any) error {
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals m into v.
|
||||
func (u *Unmarshaler) Unmarshal(i any, v any) error {
|
||||
func (u *Unmarshaler) Unmarshal(i, v any) error {
|
||||
return u.unmarshal(i, v, "")
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
|
||||
valueType := reflect.TypeOf(v)
|
||||
if valueType.Kind() != reflect.Ptr {
|
||||
return errValueNotSettable
|
||||
@@ -85,13 +93,13 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
|
||||
return errTypeMismatch
|
||||
}
|
||||
|
||||
return u.UnmarshalValuer(mapValuer(iv), v)
|
||||
return u.unmarshalValuer(mapValuer(iv), v, fullName)
|
||||
case []any:
|
||||
if elemType.Kind() != reflect.Slice {
|
||||
return errTypeMismatch
|
||||
}
|
||||
|
||||
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv)
|
||||
return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
|
||||
default:
|
||||
return errUnsupportedType
|
||||
}
|
||||
@@ -99,17 +107,21 @@ func (u *Unmarshaler) Unmarshal(i any, v any) error {
|
||||
|
||||
// UnmarshalValuer unmarshals m into v.
|
||||
func (u *Unmarshaler) UnmarshalValuer(m Valuer, v any) error {
|
||||
return u.unmarshalWithFullName(simpleValuer{current: m}, v, "")
|
||||
return u.unmarshalValuer(simpleValuer{current: m}, v, "")
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any) error {
|
||||
func (u *Unmarshaler) unmarshalValuer(m Valuer, v any, fullName string) error {
|
||||
return u.unmarshalWithFullName(simpleValuer{current: m}, v, fullName)
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillMap(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
fieldKeyType := fieldType.Key()
|
||||
fieldElemType := fieldType.Elem()
|
||||
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue)
|
||||
targetValue, err := u.generateMap(fieldKeyType, fieldElemType, mapValue, fullName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -143,14 +155,14 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any) error {
|
||||
func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, mapValue any, fullName string) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
refValue := reflect.ValueOf(mapValue)
|
||||
if refValue.Kind() != reflect.Slice {
|
||||
return errTypeMismatch
|
||||
return newTypeMismatchErrorWithHint(fullName, reflect.Slice.String(), refValue.Type().String())
|
||||
}
|
||||
if refValue.IsNil() {
|
||||
return nil
|
||||
@@ -173,6 +185,8 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
||||
}
|
||||
|
||||
valid = true
|
||||
sliceFullName := fmt.Sprintf("%s[%d]", fullName, i)
|
||||
|
||||
switch dereffedBaseKind {
|
||||
case reflect.Struct:
|
||||
target := reflect.New(dereffedBaseType)
|
||||
@@ -181,17 +195,17 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
||||
return errTypeMismatch
|
||||
}
|
||||
|
||||
if err := u.Unmarshal(val, target.Interface()); err != nil {
|
||||
if err := u.unmarshal(val, target.Interface(), sliceFullName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
SetValue(fieldType.Elem(), conv.Index(i), target.Elem())
|
||||
case reflect.Slice:
|
||||
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue); err != nil {
|
||||
if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue); err != nil {
|
||||
if err := u.fillSliceValue(conv, i, dereffedBaseKind, ithValue, sliceFullName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -205,7 +219,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value, map
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.Value,
|
||||
mapValue any) error {
|
||||
mapValue any, fullName string) error {
|
||||
var slice []any
|
||||
switch v := mapValue.(type) {
|
||||
case fmt.Stringer:
|
||||
@@ -225,7 +239,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
|
||||
|
||||
for i := 0; i < len(slice); i++ {
|
||||
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i]); err != nil {
|
||||
if err := u.fillSliceValue(conv, i, baseFieldKind, slice[i], fullName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -235,7 +249,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
|
||||
baseKind reflect.Kind, value any) error {
|
||||
baseKind reflect.Kind, value any, fullName string) error {
|
||||
ithVal := slice.Index(index)
|
||||
switch v := value.(type) {
|
||||
case fmt.Stringer:
|
||||
@@ -243,7 +257,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
|
||||
case string:
|
||||
return setValueFromString(baseKind, ithVal, v)
|
||||
case map[string]any:
|
||||
return u.fillMap(ithVal.Type(), ithVal, value)
|
||||
return u.fillMap(ithVal.Type(), ithVal, value, fullName)
|
||||
default:
|
||||
// don't need to consider the difference between int, int8, int16, int32, int64,
|
||||
// uint, uint8, uint16, uint32, uint64, because they're handled as json.Number.
|
||||
@@ -269,7 +283,7 @@ func (u *Unmarshaler) fillSliceValue(slice reflect.Value, index int,
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value reflect.Value,
|
||||
defaultValue string) error {
|
||||
defaultValue, fullName string) error {
|
||||
baseFieldType := Deref(derefedType.Elem())
|
||||
baseFieldKind := baseFieldType.Kind()
|
||||
defaultCacheLock.Lock()
|
||||
@@ -287,10 +301,10 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
|
||||
defaultCacheLock.Unlock()
|
||||
}
|
||||
|
||||
return u.fillSlice(derefedType, value, slice)
|
||||
return u.fillSlice(derefedType, value, slice, fullName)
|
||||
}
|
||||
|
||||
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any) (reflect.Value, error) {
|
||||
func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any, fullName string) (reflect.Value, error) {
|
||||
mapType := reflect.MapOf(keyType, elemType)
|
||||
valueType := reflect.TypeOf(mapValue)
|
||||
if mapType == valueType {
|
||||
@@ -309,11 +323,12 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
for _, key := range refValue.MapKeys() {
|
||||
keythValue := refValue.MapIndex(key)
|
||||
keythData := keythValue.Interface()
|
||||
mapFullName := fmt.Sprintf("%s[%s]", fullName, key.String())
|
||||
|
||||
switch dereffedElemKind {
|
||||
case reflect.Slice:
|
||||
target := reflect.New(dereffedElemType)
|
||||
if err := u.fillSlice(elemType, target.Elem(), keythData); err != nil {
|
||||
if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil {
|
||||
return emptyValue, err
|
||||
}
|
||||
|
||||
@@ -325,7 +340,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
}
|
||||
|
||||
target := reflect.New(dereffedElemType)
|
||||
if err := u.Unmarshal(keythMap, target.Interface()); err != nil {
|
||||
if err := u.unmarshal(keythMap, target.Interface(), mapFullName); err != nil {
|
||||
return emptyValue, err
|
||||
}
|
||||
|
||||
@@ -336,7 +351,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any)
|
||||
return emptyValue, errTypeMismatch
|
||||
}
|
||||
|
||||
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap)
|
||||
innerValue, err := u.generateMap(elemType.Key(), elemType.Elem(), keythMap, mapFullName)
|
||||
if err != nil {
|
||||
return emptyValue, err
|
||||
}
|
||||
@@ -425,6 +440,10 @@ func (u *Unmarshaler) processAnonymousField(field reflect.StructField, value ref
|
||||
return err
|
||||
}
|
||||
|
||||
if key == ignoreKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
if options.optional() {
|
||||
return u.processAnonymousFieldOptional(field, value, key, m, fullName)
|
||||
}
|
||||
@@ -483,7 +502,7 @@ func (u *Unmarshaler) processAnonymousStructFieldOptional(fieldType reflect.Type
|
||||
return err
|
||||
}
|
||||
|
||||
_, hasValue := getValue(m, fieldKey)
|
||||
_, hasValue := getValue(m, fieldKey, u.opts.opaqueKeys)
|
||||
if hasValue {
|
||||
if !filled {
|
||||
filled = true
|
||||
@@ -541,13 +560,13 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
|
||||
parent: vp.parent,
|
||||
}, fullName)
|
||||
case typeKind == reflect.Slice && valueKind == reflect.Slice:
|
||||
return u.fillSlice(fieldType, value, mapValue)
|
||||
return u.fillSlice(fieldType, value, mapValue, fullName)
|
||||
case valueKind == reflect.Map && typeKind == reflect.Map:
|
||||
return u.fillMap(fieldType, value, mapValue)
|
||||
return u.fillMap(fieldType, value, mapValue, fullName)
|
||||
case valueKind == reflect.String && typeKind == reflect.Map:
|
||||
return u.fillMapFromString(value, mapValue)
|
||||
case valueKind == reflect.String && typeKind == reflect.Slice:
|
||||
return u.fillSliceFromString(fieldType, value, mapValue)
|
||||
return u.fillSliceFromString(fieldType, value, mapValue, fullName)
|
||||
case valueKind == reflect.String && derefedFieldType == durationType:
|
||||
return fillDurationValue(fieldType, value, mapValue.(string))
|
||||
default:
|
||||
@@ -592,25 +611,23 @@ func (u *Unmarshaler) processFieldPrimitiveWithJSONNumber(fieldType reflect.Type
|
||||
target := reflect.New(Deref(fieldType)).Elem()
|
||||
|
||||
switch typeKind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
iValue, err := v.Int64()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if err := setValueFromString(typeKind, target, v.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Float32:
|
||||
fValue, err := v.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target.SetInt(iValue)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
iValue, err := v.Int64()
|
||||
if err != nil {
|
||||
return err
|
||||
if fValue > math.MaxFloat32 {
|
||||
return fmt.Errorf("parsing %q as float32: value out of range", v.String())
|
||||
}
|
||||
|
||||
if iValue < 0 {
|
||||
return fmt.Errorf("unmarshal %q with bad value %q", fullName, v.String())
|
||||
}
|
||||
|
||||
target.SetUint(uint64(iValue))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
target.SetFloat(fValue)
|
||||
case reflect.Float64:
|
||||
fValue, err := v.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -618,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)
|
||||
@@ -712,6 +729,10 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
|
||||
return err
|
||||
}
|
||||
|
||||
if key == ignoreKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
fullName = join(fullName, key)
|
||||
if opts != nil && len(opts.EnvVar) > 0 {
|
||||
envVal := proc.Env(opts.EnvVar)
|
||||
@@ -726,7 +747,7 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
|
||||
}
|
||||
|
||||
valuer := createValuer(m, opts)
|
||||
mapValue, hasValue := getValue(valuer, canonicalKey)
|
||||
mapValue, hasValue := getValue(valuer, canonicalKey, u.opts.opaqueKeys)
|
||||
|
||||
// When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault.
|
||||
if u.opts.fillDefault {
|
||||
@@ -819,7 +840,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
|
||||
|
||||
switch fieldKind {
|
||||
case reflect.Array, reflect.Slice:
|
||||
return u.fillSliceWithDefault(derefedType, value, defaultValue)
|
||||
return u.fillSliceWithDefault(derefedType, value, defaultValue, fullName)
|
||||
default:
|
||||
return setValueFromString(fieldKind, value, defaultValue)
|
||||
}
|
||||
@@ -867,7 +888,7 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
|
||||
|
||||
func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName string) error {
|
||||
rv := reflect.ValueOf(v)
|
||||
if err := ValidatePtr(&rv); err != nil {
|
||||
if err := ValidatePtr(rv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -889,11 +910,6 @@ func (u *Unmarshaler) unmarshalWithFullName(m valuerWithParent, v any, fullName
|
||||
typeField := baseType.Field(i)
|
||||
valueField := valElem.Field(i)
|
||||
if err := u.processField(typeField, valueField, m, fullName); err != nil {
|
||||
if len(fullName) > 0 {
|
||||
err = fmt.Errorf("%w, fullName: %s, field: %s, type: %s",
|
||||
err, fullName, typeField.Name, valueField.Type().Name())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -922,6 +938,14 @@ func WithDefault() UnmarshalOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithOpaqueKeys customizes an Unmarshaler with opaque keys.
|
||||
// Opaque keys are keys that are not processed by the unmarshaler.
|
||||
func WithOpaqueKeys() UnmarshalOption {
|
||||
return func(opt *unmarshalOptions) {
|
||||
opt.opaqueKeys = true
|
||||
}
|
||||
}
|
||||
|
||||
func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent {
|
||||
if opts.inherit() {
|
||||
return recursiveValuer{
|
||||
@@ -999,8 +1023,8 @@ func fillWithSameType(fieldType reflect.Type, value reflect.Value, mapValue any,
|
||||
}
|
||||
|
||||
// getValue gets the value for the specific key, the key can be in the format of parentKey.childKey
|
||||
func getValue(m valuerWithParent, key string) (any, bool) {
|
||||
keys := readKeys(key)
|
||||
func getValue(m valuerWithParent, key string, opaque bool) (any, bool) {
|
||||
keys := readKeys(key, opaque)
|
||||
return getValueWithChainedKeys(m, keys)
|
||||
}
|
||||
|
||||
@@ -1059,7 +1083,11 @@ func newTypeMismatchErrorWithHint(name, expectType, actualType string) error {
|
||||
name, expectType, actualType)
|
||||
}
|
||||
|
||||
func readKeys(key string) []string {
|
||||
func readKeys(key string, opaque bool) []string {
|
||||
if opaque {
|
||||
return []string{key}
|
||||
}
|
||||
|
||||
cacheKeysLock.Lock()
|
||||
keys, ok := cacheKeys[key]
|
||||
cacheKeysLock.Unlock()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ const (
|
||||
leftSquareBracket = '['
|
||||
rightSquareBracket = ']'
|
||||
segmentSeparator = ','
|
||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -79,7 +80,7 @@ func SetMapIndexValue(tp reflect.Type, value, key, target reflect.Value) {
|
||||
}
|
||||
|
||||
// ValidatePtr validates v if it's a valid pointer.
|
||||
func ValidatePtr(v *reflect.Value) error {
|
||||
func ValidatePtr(v reflect.Value) error {
|
||||
// sequence is very important, IsNil must be called after checking Kind() with reflect.Ptr,
|
||||
// panic otherwise
|
||||
if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() {
|
||||
@@ -100,27 +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, fmt.Errorf("the value %q cannot parsed as int", str)
|
||||
}
|
||||
|
||||
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, fmt.Errorf("the value %q cannot parsed as uint", str)
|
||||
}
|
||||
|
||||
return uintValue, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(str, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the value %q cannot parsed as float", str)
|
||||
}
|
||||
|
||||
return floatValue, 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:
|
||||
return strconv.ParseFloat(str, 32)
|
||||
case reflect.Float64:
|
||||
return strconv.ParseFloat(str, 64)
|
||||
case reflect.String:
|
||||
return str, nil
|
||||
default:
|
||||
@@ -486,19 +490,22 @@ func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) err
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
value.SetBool(v.(bool))
|
||||
return nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
value.SetInt(v.(int64))
|
||||
return nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
value.SetUint(v.(uint64))
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
value.SetFloat(v.(float64))
|
||||
return nil
|
||||
case reflect.String:
|
||||
value.SetString(v.(string))
|
||||
return nil
|
||||
default:
|
||||
return errUnsupportedType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error {
|
||||
@@ -575,7 +582,8 @@ func usingDifferentKeys(key string, field reflect.StructField) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string, opts *fieldOptionsWithContext) error {
|
||||
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string,
|
||||
opts *fieldOptionsWithContext) error {
|
||||
if !value.CanSet() {
|
||||
return errValueNotSettable
|
||||
}
|
||||
|
||||
@@ -218,25 +218,25 @@ func TestParseSegments(t *testing.T) {
|
||||
func TestValidatePtrWithNonPtr(t *testing.T) {
|
||||
var foo string
|
||||
rve := reflect.ValueOf(foo)
|
||||
assert.NotNil(t, ValidatePtr(&rve))
|
||||
assert.NotNil(t, ValidatePtr(rve))
|
||||
}
|
||||
|
||||
func TestValidatePtrWithPtr(t *testing.T) {
|
||||
var foo string
|
||||
rve := reflect.ValueOf(&foo)
|
||||
assert.Nil(t, ValidatePtr(&rve))
|
||||
assert.Nil(t, ValidatePtr(rve))
|
||||
}
|
||||
|
||||
func TestValidatePtrWithNilPtr(t *testing.T) {
|
||||
var foo *string
|
||||
rve := reflect.ValueOf(foo)
|
||||
assert.NotNil(t, ValidatePtr(&rve))
|
||||
assert.NotNil(t, ValidatePtr(rve))
|
||||
}
|
||||
|
||||
func TestValidatePtrWithZeroValue(t *testing.T) {
|
||||
var s string
|
||||
e := reflect.Zero(reflect.TypeOf(s))
|
||||
assert.NotNil(t, ValidatePtr(&e))
|
||||
assert.NotNil(t, ValidatePtr(e))
|
||||
}
|
||||
|
||||
func TestSetValueNotSettable(t *testing.T) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
)
|
||||
|
||||
func TestNewHistogramVec(t *testing.T) {
|
||||
@@ -15,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)
|
||||
@@ -29,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).
|
||||
@@ -39,15 +39,13 @@ 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))
|
||||
assert.Nil(t, err)
|
||||
|
||||
proc.Shutdown()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
62
core/metric/summary.go
Normal file
62
core/metric/summary.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
prom "github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
)
|
||||
|
||||
type (
|
||||
// A SummaryVecOpts is a summary vector options
|
||||
SummaryVecOpts struct {
|
||||
VecOpt VectorOpts
|
||||
Objectives map[float64]float64
|
||||
}
|
||||
|
||||
// A SummaryVec interface represents a summary vector.
|
||||
SummaryVec interface {
|
||||
// Observe adds observation v to labels.
|
||||
Observe(v float64, labels ...string)
|
||||
close() bool
|
||||
}
|
||||
|
||||
promSummaryVec struct {
|
||||
summary *prom.SummaryVec
|
||||
}
|
||||
)
|
||||
|
||||
// NewSummaryVec return a SummaryVec
|
||||
func NewSummaryVec(cfg *SummaryVecOpts) SummaryVec {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
vec := prom.NewSummaryVec(
|
||||
prom.SummaryOpts{
|
||||
Namespace: cfg.VecOpt.Namespace,
|
||||
Subsystem: cfg.VecOpt.Subsystem,
|
||||
Name: cfg.VecOpt.Name,
|
||||
Help: cfg.VecOpt.Help,
|
||||
Objectives: cfg.Objectives,
|
||||
},
|
||||
cfg.VecOpt.Labels,
|
||||
)
|
||||
prom.MustRegister(vec)
|
||||
sv := &promSummaryVec{
|
||||
summary: vec,
|
||||
}
|
||||
proc.AddShutdownListener(func() {
|
||||
sv.close()
|
||||
})
|
||||
|
||||
return sv
|
||||
}
|
||||
|
||||
func (sv *promSummaryVec) Observe(v float64, labels ...string) {
|
||||
update(func() {
|
||||
sv.summary.WithLabelValues(labels...).Observe(v)
|
||||
})
|
||||
}
|
||||
|
||||
func (sv *promSummaryVec) close() bool {
|
||||
return prom.Unregister(sv.summary)
|
||||
}
|
||||
68
core/metric/summary_test.go
Normal file
68
core/metric/summary_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
)
|
||||
|
||||
func TestNewSummaryVec(t *testing.T) {
|
||||
summaryVec := NewSummaryVec(&SummaryVecOpts{
|
||||
VecOpt: VectorOpts{
|
||||
Namespace: "http_server",
|
||||
Subsystem: "requests",
|
||||
Name: "duration_quantiles",
|
||||
Help: "rpc client requests duration(ms) φ quantiles ",
|
||||
Labels: []string{"method"},
|
||||
},
|
||||
Objectives: map[float64]float64{
|
||||
0.5: 0.01,
|
||||
0.9: 0.01,
|
||||
},
|
||||
})
|
||||
defer summaryVec.close()
|
||||
summaryVecNil := NewSummaryVec(nil)
|
||||
assert.NotNil(t, summaryVec)
|
||||
assert.Nil(t, summaryVecNil)
|
||||
}
|
||||
|
||||
func TestSummaryObserve(t *testing.T) {
|
||||
startAgent()
|
||||
summaryVec := NewSummaryVec(&SummaryVecOpts{
|
||||
VecOpt: VectorOpts{
|
||||
Namespace: "http_server",
|
||||
Subsystem: "requests",
|
||||
Name: "duration_quantiles",
|
||||
Help: "rpc client requests duration(ms) φ quantiles ",
|
||||
Labels: []string{"method"},
|
||||
},
|
||||
Objectives: map[float64]float64{
|
||||
0.3: 0.01,
|
||||
0.6: 0.01,
|
||||
1: 0.01,
|
||||
},
|
||||
})
|
||||
defer summaryVec.close()
|
||||
sv := summaryVec.(*promSummaryVec)
|
||||
sv.Observe(100, "GET")
|
||||
sv.Observe(200, "GET")
|
||||
sv.Observe(300, "GET")
|
||||
metadata := `
|
||||
# HELP http_server_requests_duration_quantiles rpc client requests duration(ms) φ quantiles
|
||||
# TYPE http_server_requests_duration_quantiles summary
|
||||
`
|
||||
val := `
|
||||
http_server_requests_duration_quantiles{method="GET",quantile="0.3"} 100
|
||||
http_server_requests_duration_quantiles{method="GET",quantile="0.6"} 200
|
||||
http_server_requests_duration_quantiles{method="GET",quantile="1"} 300
|
||||
http_server_requests_duration_quantiles_sum{method="GET"} 600
|
||||
http_server_requests_duration_quantiles_count{method="GET"} 3
|
||||
`
|
||||
|
||||
err := testutil.CollectAndCompare(sv.summary, strings.NewReader(metadata+val))
|
||||
assert.Nil(t, err)
|
||||
proc.Shutdown()
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -96,4 +96,6 @@ func (lm *listenerManager) notifyListeners() {
|
||||
group.RunSafe(listener)
|
||||
}
|
||||
group.Wait()
|
||||
|
||||
lm.listeners = nil
|
||||
}
|
||||
|
||||
@@ -28,3 +28,33 @@ func TestShutdown(t *testing.T) {
|
||||
called()
|
||||
assert.Equal(t, 3, val)
|
||||
}
|
||||
|
||||
func TestNotifyMoreThanOnce(t *testing.T) {
|
||||
ch := make(chan struct{}, 1)
|
||||
|
||||
go func() {
|
||||
var val int
|
||||
called := AddWrapUpListener(func() {
|
||||
val++
|
||||
})
|
||||
WrapUp()
|
||||
WrapUp()
|
||||
called()
|
||||
assert.Equal(t, 1, val)
|
||||
|
||||
called = AddShutdownListener(func() {
|
||||
val += 2
|
||||
})
|
||||
Shutdown()
|
||||
Shutdown()
|
||||
called()
|
||||
assert.Equal(t, 3, val)
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout, check error logs")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,23 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
const timeFormat = "0102150405"
|
||||
const (
|
||||
profileDuration = time.Minute
|
||||
timeFormat = "0102150405"
|
||||
)
|
||||
|
||||
var done = make(chan struct{})
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
var profiler Stopper
|
||||
|
||||
// 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
|
||||
@@ -28,21 +30,14 @@ func init() {
|
||||
case syscall.SIGUSR1:
|
||||
dumpGoroutines(fileCreator{})
|
||||
case syscall.SIGUSR2:
|
||||
if profiler == nil {
|
||||
profiler = StartProfile()
|
||||
} else {
|
||||
profiler.Stop()
|
||||
profiler = nil
|
||||
}
|
||||
profiler := StartProfile()
|
||||
time.AfterFunc(profileDuration, profiler.Stop)
|
||||
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 +49,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()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/proc"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"github.com/zeromicro/go-zero/core/threading"
|
||||
@@ -51,7 +50,7 @@ func (sg *ServiceGroup) Add(service Service) {
|
||||
// Also, quitting this method will close the logx output.
|
||||
func (sg *ServiceGroup) Start() {
|
||||
proc.AddShutdownListener(func() {
|
||||
log.Println("Shutting down...")
|
||||
logx.Info("Shutting down services in group")
|
||||
sg.stopOnce()
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
|
||||
func TestBulkInserter(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "ok", Value: 1}}...))
|
||||
bulk, err := NewBulkInserter(createModel(mt).Collection)
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
|
||||
func TestClientManger_getClient(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
Inject(mtest.ClusterURI(), mt.Client)
|
||||
cli, err := getClient(mtest.ClusterURI())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ func TestKeepPromise_keep(t *testing.T) {
|
||||
|
||||
func TestNewCollection(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
coll := mt.Coll
|
||||
assert.NotNil(t, coll)
|
||||
@@ -79,7 +78,6 @@ func TestNewCollection(t *testing.T) {
|
||||
|
||||
func TestCollection_Aggregate(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
coll := mt.Coll
|
||||
assert.NotNil(t, coll)
|
||||
@@ -96,8 +94,6 @@ func TestCollection_Aggregate(t *testing.T) {
|
||||
|
||||
func TestCollection_BulkWrite(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -119,8 +115,6 @@ func TestCollection_BulkWrite(t *testing.T) {
|
||||
|
||||
func TestCollection_CountDocuments(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -145,8 +139,6 @@ func TestCollection_CountDocuments(t *testing.T) {
|
||||
|
||||
func TestDecoratedCollection_DeleteMany(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -165,8 +157,6 @@ func TestDecoratedCollection_DeleteMany(t *testing.T) {
|
||||
|
||||
func TestCollection_Distinct(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -185,8 +175,6 @@ func TestCollection_Distinct(t *testing.T) {
|
||||
|
||||
func TestCollection_EstimatedDocumentCount(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -205,8 +193,6 @@ func TestCollection_EstimatedDocumentCount(t *testing.T) {
|
||||
|
||||
func TestCollection_Find(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -253,8 +239,6 @@ func TestCollection_Find(t *testing.T) {
|
||||
|
||||
func TestCollection_FindOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -297,8 +281,6 @@ func TestCollection_FindOne(t *testing.T) {
|
||||
|
||||
func TestCollection_FindOneAndDelete(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -328,8 +310,6 @@ func TestCollection_FindOneAndDelete(t *testing.T) {
|
||||
|
||||
func TestCollection_FindOneAndReplace(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -360,8 +340,6 @@ func TestCollection_FindOneAndReplace(t *testing.T) {
|
||||
|
||||
func TestCollection_FindOneAndUpdate(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -393,8 +371,6 @@ func TestCollection_FindOneAndUpdate(t *testing.T) {
|
||||
|
||||
func TestCollection_InsertOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -413,8 +389,6 @@ func TestCollection_InsertOne(t *testing.T) {
|
||||
|
||||
func TestCollection_InsertMany(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -437,8 +411,6 @@ func TestCollection_InsertMany(t *testing.T) {
|
||||
|
||||
func TestCollection_DeleteOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -457,8 +429,6 @@ func TestCollection_DeleteOne(t *testing.T) {
|
||||
|
||||
func TestCollection_DeleteMany(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -477,8 +447,6 @@ func TestCollection_DeleteMany(t *testing.T) {
|
||||
|
||||
func TestCollection_ReplaceOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -500,8 +468,6 @@ func TestCollection_ReplaceOne(t *testing.T) {
|
||||
|
||||
func TestCollection_UpdateOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -522,8 +488,6 @@ func TestCollection_UpdateOne(t *testing.T) {
|
||||
|
||||
func TestCollection_UpdateByID(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -544,8 +508,6 @@ func TestCollection_UpdateByID(t *testing.T) {
|
||||
|
||||
func TestCollection_UpdateMany(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
@@ -566,7 +528,6 @@ func TestCollection_UpdateMany(t *testing.T) {
|
||||
|
||||
func TestDecoratedCollection_LogDuration(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
c := decoratedCollection{
|
||||
Collection: mt.Coll,
|
||||
brk: breaker.NewBreaker(),
|
||||
@@ -599,13 +560,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)
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
|
||||
func TestModel_StartSession(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
sess, err := m.StartSession()
|
||||
@@ -34,8 +32,6 @@ func TestModel_StartSession(t *testing.T) {
|
||||
|
||||
func TestModel_Aggregate(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
find := mtest.CreateCursorResponse(
|
||||
@@ -71,8 +67,6 @@ func TestModel_Aggregate(t *testing.T) {
|
||||
|
||||
func TestModel_DeleteMany(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
|
||||
@@ -88,8 +82,6 @@ func TestModel_DeleteMany(t *testing.T) {
|
||||
|
||||
func TestModel_DeleteOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
|
||||
@@ -105,8 +97,6 @@ func TestModel_DeleteOne(t *testing.T) {
|
||||
|
||||
func TestModel_Find(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
find := mtest.CreateCursorResponse(
|
||||
@@ -142,8 +132,6 @@ func TestModel_Find(t *testing.T) {
|
||||
|
||||
func TestModel_FindOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
find := mtest.CreateCursorResponse(
|
||||
@@ -170,8 +158,6 @@ func TestModel_FindOne(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndDelete(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
@@ -189,8 +175,6 @@ func TestModel_FindOneAndDelete(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndReplace(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
@@ -212,8 +196,6 @@ func TestModel_FindOneAndReplace(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndUpdate(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(mt)
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
|
||||
func TestNewModel(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
_, err := newModel("foo", mt.DB.Name(), mt.Coll.Name(), nil)
|
||||
assert.NotNil(mt, err)
|
||||
@@ -27,8 +25,6 @@ func TestNewModel(t *testing.T) {
|
||||
|
||||
func TestModel_DelCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(t, mt)
|
||||
assert.Nil(t, m.cache.Set("foo", "bar"))
|
||||
@@ -42,8 +38,6 @@ func TestModel_DelCache(t *testing.T) {
|
||||
|
||||
func TestModel_DeleteOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
|
||||
m := createModel(t, mt)
|
||||
@@ -65,8 +59,6 @@ func TestModel_DeleteOne(t *testing.T) {
|
||||
|
||||
func TestModel_DeleteOneNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
|
||||
m := createModel(t, mt)
|
||||
@@ -81,8 +73,6 @@ func TestModel_DeleteOneNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_FindOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
resp := mtest.CreateCursorResponse(
|
||||
1,
|
||||
@@ -104,8 +94,6 @@ func TestModel_FindOne(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
resp := mtest.CreateCursorResponse(
|
||||
1,
|
||||
@@ -126,8 +114,6 @@ func TestModel_FindOneNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndDelete(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -152,8 +138,6 @@ func TestModel_FindOneAndDelete(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndDeleteNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -169,8 +153,6 @@ func TestModel_FindOneAndDeleteNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndReplace(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -201,8 +183,6 @@ func TestModel_FindOneAndReplace(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndReplaceNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -220,8 +200,6 @@ func TestModel_FindOneAndReplaceNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndUpdate(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -252,8 +230,6 @@ func TestModel_FindOneAndUpdate(t *testing.T) {
|
||||
|
||||
func TestModel_FindOneAndUpdateNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -271,8 +247,6 @@ func TestModel_FindOneAndUpdateNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_GetCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(t, mt)
|
||||
assert.NotNil(t, m.cache)
|
||||
@@ -285,8 +259,6 @@ func TestModel_GetCache(t *testing.T) {
|
||||
|
||||
func TestModel_InsertOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -318,8 +290,6 @@ func TestModel_InsertOne(t *testing.T) {
|
||||
|
||||
func TestModel_InsertOneNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -335,8 +305,6 @@ func TestModel_InsertOneNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_ReplaceOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -368,8 +336,6 @@ func TestModel_ReplaceOne(t *testing.T) {
|
||||
|
||||
func TestModel_ReplaceOneNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -385,8 +351,6 @@ func TestModel_ReplaceOneNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_SetCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
m := createModel(t, mt)
|
||||
assert.Nil(t, m.SetCache("foo", "bar"))
|
||||
@@ -398,8 +362,6 @@ func TestModel_SetCache(t *testing.T) {
|
||||
|
||||
func TestModel_UpdateByID(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -431,8 +393,6 @@ func TestModel_UpdateByID(t *testing.T) {
|
||||
|
||||
func TestModel_UpdateByIDNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -448,8 +408,6 @@ func TestModel_UpdateByIDNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_UpdateMany(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -483,8 +441,6 @@ func TestModel_UpdateMany(t *testing.T) {
|
||||
|
||||
func TestModel_UpdateManyNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -500,8 +456,6 @@ func TestModel_UpdateManyNoCache(t *testing.T) {
|
||||
|
||||
func TestModel_UpdateOne(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
@@ -533,8 +487,6 @@ func TestModel_UpdateOne(t *testing.T) {
|
||||
|
||||
func TestModel_UpdateOneNoCache(t *testing.T) {
|
||||
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
|
||||
defer mt.Close()
|
||||
|
||||
mt.Run("test", func(mt *mtest.T) {
|
||||
mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
|
||||
{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -467,6 +467,33 @@ func (s *Redis) ExistsCtx(ctx context.Context, key string) (val bool, err error)
|
||||
return
|
||||
}
|
||||
|
||||
// ExistsMany is the implementation of redis exists command.
|
||||
// checks the existence of multiple keys in Redis using the EXISTS command.
|
||||
func (s *Redis) ExistsMany(keys ...string) (int64, error) {
|
||||
return s.ExistsManyCtx(context.Background(), keys...)
|
||||
}
|
||||
|
||||
// ExistsManyCtx is the implementation of redis exists command.
|
||||
// checks the existence of multiple keys in Redis using the EXISTS command.
|
||||
func (s *Redis) ExistsManyCtx(ctx context.Context, keys ...string) (val int64, err error) {
|
||||
err = s.brk.DoWithAcceptable(func() error {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := conn.Exists(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val = v
|
||||
return nil
|
||||
}, acceptable)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Expire is the implementation of redis expire command.
|
||||
func (s *Redis) Expire(key string, seconds int) error {
|
||||
return s.ExpireCtx(context.Background(), key, seconds)
|
||||
@@ -1993,7 +2020,13 @@ func (s *Redis) TtlCtx(ctx context.Context, key string) (val int, err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
val = int(duration / time.Second)
|
||||
if duration >= 0 {
|
||||
val = int(duration / time.Second)
|
||||
} else {
|
||||
// -2 means key does not exist
|
||||
// -1 means key exists but has no expire
|
||||
val = int(duration)
|
||||
}
|
||||
return nil
|
||||
}, acceptable)
|
||||
|
||||
@@ -2849,7 +2882,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) {
|
||||
|
||||
@@ -224,6 +224,36 @@ func TestRedisTLS_Exists(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedis_ExistsMany(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
// Attempt to create a new Redis instance with an incorrect type and call ExistsMany
|
||||
_, err := New(client.Addr, badType()).ExistsMany("key1", "key2")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// Check if key1 and key2 exist, expecting that they do not
|
||||
val, err := client.ExistsMany("key1", "key2")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), val)
|
||||
|
||||
// Set the value for key1 and check if key1 exists
|
||||
assert.Nil(t, client.Set("key1", "value1"))
|
||||
val, err = client.ExistsMany("key1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(1), val)
|
||||
|
||||
// Set the value for key2 and check if key1 and key2 exist
|
||||
assert.Nil(t, client.Set("key2", "value2"))
|
||||
val, err = client.ExistsMany("key1", "key2")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(2), val)
|
||||
|
||||
// Check if key1, key2, and a non-existent key3 exist, expecting that key1 and key2 do
|
||||
val, err = client.ExistsMany("key1", "key2", "key3")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(2), val)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedis_Eval(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
_, err := New(client.Addr, badType()).Eval(`redis.call("EXISTS", KEYS[1])`, []string{"notexist"})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,6 +3,8 @@ package redis
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
red "github.com/go-redis/redis/v8"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -41,3 +43,17 @@ func TestSplitClusterAddrs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCluster(t *testing.T) {
|
||||
r := miniredis.RunT(t)
|
||||
defer r.Close()
|
||||
c, err := getCluster(&Redis{
|
||||
Addr: r.Addr(),
|
||||
Type: ClusterType,
|
||||
tls: true,
|
||||
hooks: []red.Hook{durationHook},
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ import (
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stat"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/dbtest"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis/redistest"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"github.com/zeromicro/go-zero/internal/dbtest"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -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,22 +5,30 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/internal/dbtest"
|
||||
"github.com/zeromicro/go-zero/core/stores/dbtest"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,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
|
||||
}
|
||||
|
||||
@@ -182,7 +182,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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/internal/dbtest"
|
||||
"github.com/zeromicro/go-zero/core/stores/dbtest"
|
||||
)
|
||||
|
||||
func TestUnmarshalRowBool(t *testing.T) {
|
||||
|
||||
@@ -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,12 +292,21 @@ func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Contex
|
||||
}
|
||||
|
||||
func (db *commonSqlConn) acceptable(err error) bool {
|
||||
ok := err == nil || err == sql.ErrNoRows || err == sql.ErrTxDone || err == context.Canceled
|
||||
if db.accept == nil {
|
||||
return ok
|
||||
if err == nil || errors.Is(err, sql.ErrNoRows) || errors.Is(err, sql.ErrTxDone) ||
|
||||
errors.Is(err, context.Canceled) {
|
||||
return true
|
||||
}
|
||||
|
||||
return ok || db.accept(err)
|
||||
var e acceptableError
|
||||
if errors.As(err, &e) {
|
||||
return true
|
||||
}
|
||||
|
||||
if db.accept == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return db.accept(err)
|
||||
}
|
||||
|
||||
func (db *commonSqlConn) queryRows(ctx context.Context, scanner func(*sql.Rows) error,
|
||||
@@ -314,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")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/dbtest"
|
||||
"github.com/zeromicro/go-zero/core/trace/tracetest"
|
||||
"github.com/zeromicro/go-zero/internal/dbtest"
|
||||
)
|
||||
|
||||
const mockedDatasource = "sqlmock"
|
||||
@@ -236,6 +236,33 @@ func TestStatement(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBreakerWithFormatError(t *testing.T) {
|
||||
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
||||
conn := NewSqlConnFromDB(db, withMysqlAcceptable())
|
||||
for i := 0; i < 1000; i++ {
|
||||
var val string
|
||||
if !assert.NotEqual(t, breaker.ErrServiceUnavailable,
|
||||
conn.QueryRow(&val, "any ?, ?", "foo")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBreakerWithScanError(t *testing.T) {
|
||||
dbtest.RunTest(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
|
||||
conn := NewSqlConnFromDB(db, withMysqlAcceptable())
|
||||
for i := 0; i < 1000; i++ {
|
||||
rows := sqlmock.NewRows([]string{"foo"}).AddRow("bar")
|
||||
mock.ExpectQuery("any").WillReturnRows(rows)
|
||||
var val int
|
||||
if !assert.NotEqual(t, breaker.ErrServiceUnavailable, conn.QueryRow(&val, "any")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func buildConn() (mock sqlmock.Sqlmock, err error) {
|
||||
_, err = connManager.GetResource(mockedDatasource, func() (io.Closer, error) {
|
||||
var db *sql.DB
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
"github.com/zeromicro/go-zero/internal/dbtest"
|
||||
"github.com/zeromicro/go-zero/core/stores/dbtest"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -51,7 +51,13 @@ func escape(input string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func format(query string, args ...any) (string, error) {
|
||||
func format(query string, args ...any) (val string, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = newAcceptableError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
numArgs := len(args)
|
||||
if numArgs == 0 {
|
||||
return query, nil
|
||||
@@ -66,7 +72,8 @@ func format(query string, args ...any) (string, error) {
|
||||
switch ch {
|
||||
case '?':
|
||||
if argIndex >= numArgs {
|
||||
return "", fmt.Errorf("%d ? in sql, but less arguments provided", argIndex)
|
||||
return "", fmt.Errorf("%d ? in sql, but only %d arguments provided",
|
||||
argIndex+1, numArgs)
|
||||
}
|
||||
|
||||
writeValue(&b, args[argIndex])
|
||||
@@ -136,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())
|
||||
}
|
||||
}
|
||||
@@ -165,3 +172,17 @@ func writeValue(buf *strings.Builder, arg any) {
|
||||
buf.WriteString(mapping.Repr(v))
|
||||
}
|
||||
}
|
||||
|
||||
type acceptableError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newAcceptableError(err error) error {
|
||||
return acceptableError{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (e acceptableError) Error() string {
|
||||
return e.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 {
|
||||
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
kindOtlpGrpc = "otlpgrpc"
|
||||
kindOtlpHttp = "otlphttp"
|
||||
kindFile = "file"
|
||||
protocolUdp = "udp"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -65,9 +66,10 @@ func createExporter(c Config) (sdktrace.SpanExporter, error) {
|
||||
// Just support jaeger and zipkin now, more for later
|
||||
switch c.Batcher {
|
||||
case kindJaeger:
|
||||
u, _ := url.Parse(c.Endpoint)
|
||||
if u.Scheme == "udp" {
|
||||
return jaeger.New(jaeger.WithAgentEndpoint(jaeger.WithAgentHost(u.Hostname()), jaeger.WithAgentPort(u.Port())))
|
||||
u, err := url.Parse(c.Endpoint)
|
||||
if err == nil && u.Scheme == protocolUdp {
|
||||
return jaeger.New(jaeger.WithAgentEndpoint(jaeger.WithAgentHost(u.Hostname()),
|
||||
jaeger.WithAgentPort(u.Port())))
|
||||
}
|
||||
return jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(c.Endpoint)))
|
||||
case kindZipkin:
|
||||
@@ -111,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...)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func NewRequestParser(r *http.Request, resolver jsonpb.AnyResolver) (grpcurl.Req
|
||||
}
|
||||
|
||||
m := make(map[string]any)
|
||||
if err := json.NewDecoder(body).Decode(&m); err != nil {
|
||||
if err := json.NewDecoder(body).Decode(&m); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user