├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql.yml │ ├── deploy.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_ZH.md ├── cmd ├── eagle │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── base │ │ │ ├── get.go │ │ │ ├── install.go │ │ │ ├── mod.go │ │ │ ├── mod_test.go │ │ │ ├── path.go │ │ │ ├── repo.go │ │ │ ├── repo_test.go │ │ │ ├── vcs_url.go │ │ │ └── vcs_url_test.go │ │ ├── cache │ │ │ ├── add │ │ │ │ ├── add.go │ │ │ │ ├── cache.go │ │ │ │ └── template.go │ │ │ └── cache.go │ │ ├── handler │ │ │ ├── add │ │ │ │ ├── add.go │ │ │ │ ├── handler.go │ │ │ │ └── template.go │ │ │ └── handler.go │ │ ├── model │ │ │ ├── model.go │ │ │ └── new.go │ │ ├── project │ │ │ ├── new.go │ │ │ └── project.go │ │ ├── proto │ │ │ ├── add │ │ │ │ ├── add.go │ │ │ │ ├── proto.go │ │ │ │ └── template.go │ │ │ ├── client │ │ │ │ └── client.go │ │ │ ├── proto.go │ │ │ └── server │ │ │ │ ├── server.go │ │ │ │ └── template.go │ │ ├── repo │ │ │ ├── add │ │ │ │ ├── add.go │ │ │ │ ├── repo.go │ │ │ │ └── template.go │ │ │ └── repo.go │ │ ├── run │ │ │ └── run.go │ │ ├── service │ │ │ ├── add │ │ │ │ ├── add.go │ │ │ │ ├── service.go │ │ │ │ └── template.go │ │ │ └── service.go │ │ ├── task │ │ │ ├── add │ │ │ │ ├── add.go │ │ │ │ ├── task.go │ │ │ │ └── template.go │ │ │ ├── list │ │ │ │ └── list.go │ │ │ └── task.go │ │ ├── upgrade │ │ │ └── upgrade.go │ │ └── utils │ │ │ └── utils.go │ └── main.go └── protoc-gen-go-gin │ ├── gin.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── template.go ├── config ├── docker │ ├── README.md │ ├── app.yaml │ ├── config.yaml │ ├── database.yaml │ ├── logger.yaml │ ├── nginx_api.conf │ ├── redis.yaml │ └── trace.yaml └── local │ ├── README.md │ ├── app.yaml │ ├── config.yaml │ ├── database.yaml │ ├── kafka.yaml │ ├── logger.yaml │ ├── nginx_api.conf │ ├── rabbitmq.yaml │ ├── redis.yaml │ └── trace.yaml ├── deploy ├── README.md ├── docker │ └── mysql │ │ ├── init.sql │ │ └── my.cnf ├── grafana │ ├── alertmanager.yml │ └── rules.yml ├── k8s │ ├── aws-auth.yaml │ ├── consul-server-http.yaml │ ├── consul-server-service.yaml │ ├── consul-server.yaml │ ├── etcd-StatefulSet.yaml │ ├── etcd-service.yaml │ ├── etcd-ui-configmap.yaml │ ├── etcd-ui-deployment-service.yaml │ ├── etcd-ui.yaml │ ├── etcd.yml.tmpl │ ├── etcd_config.bash │ ├── go-deployment.yaml │ ├── go-ingress.yaml │ └── go-service.yaml └── prometheus │ ├── README.md │ └── prometheus.yml ├── docker-compose.yml ├── docs └── images │ ├── clean-arch.png │ ├── eagle-layout-arch.excalidraw │ └── eagle-layout-arch.png ├── examples ├── config │ ├── config.yaml │ ├── main.go │ └── server.yaml ├── crontab │ ├── app.yaml │ ├── crontab.yaml │ ├── jobs.go │ ├── logger.yaml │ └── server.go ├── helloworld │ ├── README.md │ ├── client │ │ └── main.go │ ├── helloworld │ │ ├── greeter.pb.go │ │ ├── greeter.proto │ │ └── greeter_grpc.pb.go │ └── server │ │ └── main.go ├── log │ ├── logger.yaml │ └── main.go ├── queue │ ├── kafka │ │ ├── config │ │ │ ├── app.yaml │ │ │ ├── kafka.yaml │ │ │ └── logger.yaml │ │ └── main.go │ ├── nats │ │ └── main.go │ ├── rabbitmq │ │ ├── consumer │ │ │ ├── config │ │ │ │ ├── app.yaml │ │ │ │ ├── logger.yaml │ │ │ │ └── rabbitmq.yaml │ │ │ ├── main.go │ │ │ └── server.go │ │ └── producer │ │ │ ├── config │ │ │ ├── app.yaml │ │ │ ├── logger.yaml │ │ │ └── rabbitmq.yaml │ │ │ ├── delay_publish.go │ │ │ └── main.go │ └── redis │ │ ├── app.yaml │ │ ├── client.go │ │ ├── handler.go │ │ ├── logger.yaml │ │ ├── producer.go │ │ ├── redis.yaml │ │ └── server.go ├── registry │ └── main.go └── trace │ └── main.go ├── go.mod ├── go.sum ├── internal ├── cache │ ├── cache_client.go │ └── user_base.go ├── ecode │ ├── README.md │ └── user.go ├── handler │ └── v1 │ │ └── user │ │ ├── follow.go │ │ ├── follow_list.go │ │ ├── follower_list.go │ │ ├── get.go │ │ ├── login.go │ │ ├── register.go │ │ ├── unfollow.go │ │ ├── update.go │ │ ├── user.go │ │ └── vcode.go ├── middleware │ ├── README.md │ └── translations.go ├── model │ ├── README.md │ ├── init.go │ ├── model.go │ ├── user_base.go │ ├── user_fans.go │ ├── user_follow.go │ └── user_stat.go ├── pkg │ ├── README.md │ ├── counter.go │ ├── idalloc.go │ └── repeat.go ├── repository │ ├── README.md │ ├── repository.go │ ├── user_base_repo.go │ ├── user_follow_repo.go │ └── user_stat_repo.go ├── routers │ ├── router.go │ └── web.go ├── server │ ├── grpc.go │ └── http.go ├── service │ ├── README.md │ ├── common_service.go │ ├── relation_service.go │ ├── service.go │ ├── sms_service.go │ ├── sms_service_test.go │ ├── trans.go │ ├── user_service.go │ └── vcode_service.go ├── templates │ ├── README.md │ ├── error │ │ └── 404.html │ ├── index.html │ ├── layouts │ │ ├── footer.html │ │ ├── header.html │ │ └── master.html │ ├── partials │ │ └── error.html │ └── user │ │ ├── login.html │ │ └── register.html └── web │ ├── README.md │ ├── error.go │ ├── index.go │ ├── response.go │ └── user │ ├── login.go │ ├── logout.go │ ├── register.go │ └── user.go ├── main.go ├── pkg ├── app │ ├── app.go │ ├── config.go │ ├── doc.go │ ├── form.go │ ├── jwt.go │ ├── options.go │ └── response.go ├── async │ └── async.go ├── auth │ └── auth.go ├── cache │ ├── README.md │ ├── cache.go │ ├── getter.go │ ├── key.go │ ├── memory.go │ ├── memory_test.go │ ├── metric.go │ ├── redis.go │ └── redis_test.go ├── client │ ├── consulclient │ │ └── client.go │ ├── etcdclient │ │ ├── client.go │ │ └── client_test.go │ ├── httpclient │ │ ├── client.go │ │ ├── client_test.go │ │ ├── options.go │ │ └── otelhttp.go │ └── nacosclient │ │ └── client.go ├── config │ ├── README.md │ ├── config.go │ ├── config_test.go │ ├── options.go │ └── testdata │ │ └── app.yaml ├── container │ └── group │ │ ├── example_test.go │ │ ├── group.go │ │ └── group_test.go ├── email │ ├── init.go │ ├── mail.go │ ├── smtp.go │ ├── template.go │ └── templates │ │ ├── active-mail.html │ │ └── reset-mail.html ├── encoding │ ├── encoding.go │ ├── encoding_test.go │ ├── example_test.go │ ├── gob_encoding.go │ ├── json │ │ └── json.go │ ├── json_encoding.go │ ├── msgpack_encoding.go │ └── proto │ │ └── proto.go ├── errcode │ ├── README.md │ ├── code.go │ ├── error.go │ └── grpc_error.go ├── flash │ ├── message.go │ └── session.go ├── lock │ ├── etcd.go │ ├── lock.go │ ├── lock_test.go │ ├── luascript.go │ ├── redis.go │ ├── redis_test.go │ └── utils.go ├── log │ ├── README.md │ ├── config.go │ ├── logger.go │ ├── logger_test.go │ ├── options.go │ ├── span_logger.go │ ├── utils.go │ └── zap.go ├── metric │ ├── README.md │ ├── counter.go │ ├── counter_test.go │ ├── gauge.go │ ├── gauge_test.go │ ├── golang_app_dashboard.json │ ├── histogram.go │ ├── histogram_test.go │ └── metric.go ├── middleware │ ├── README.md │ ├── access_log.go │ ├── auth.go │ ├── breaker.go │ ├── cors.go │ ├── header.go │ ├── logging.go │ ├── metrics.go │ ├── middleware.go │ ├── ratelimit.go │ ├── requestid.go │ ├── sign_md5.go │ ├── timeout.go │ ├── tracing.go │ └── tracing_test.go ├── queue │ ├── README.md │ ├── interface.go │ ├── kafka │ │ ├── README.md │ │ ├── client.go │ │ ├── config.go │ │ ├── consumer.go │ │ ├── consumer_handler.go │ │ ├── manager.go │ │ └── producer.go │ ├── nats │ │ ├── consumer.go │ │ ├── init.go │ │ └── producer.go │ └── rabbitmq │ │ ├── README.md │ │ ├── channel.go │ │ ├── client.go │ │ ├── config.go │ │ ├── connection.go │ │ ├── consumer.go │ │ ├── manager.go │ │ ├── options │ │ ├── bind.go │ │ ├── connection.go │ │ ├── consumer.go │ │ ├── exchange.go │ │ ├── publish.go │ │ └── queue.go │ │ └── producer.go ├── redis │ ├── README.md │ ├── check_repeat.go │ ├── config.go │ ├── idalloc.go │ ├── idalloc_test.go │ ├── redis.go │ ├── redis_example_test.go │ └── redis_test.go ├── registry │ ├── consul │ │ ├── client.go │ │ ├── registry.go │ │ ├── registry_test.go │ │ ├── service.go │ │ └── watcher.go │ ├── etcd │ │ ├── registry.go │ │ ├── registry_test.go │ │ ├── service.go │ │ └── watcher.go │ ├── nacos │ │ ├── registry.go │ │ └── watcher.go │ └── registry.go ├── sign │ ├── README.md │ ├── keys.go │ ├── sign_aes.go │ ├── sign_hmac.go │ ├── sign_md5.go │ ├── sign_rsa.go │ ├── signer.go │ ├── singer_test.go │ ├── verifer.go │ └── verifer_test.go ├── storage │ ├── elasticsearch │ │ ├── README.md │ │ ├── client.go │ │ ├── client_test.go │ │ ├── document.go │ │ ├── index.go │ │ └── search.go │ ├── mongodb │ │ └── mongodb.go │ ├── orm │ │ ├── log_writer.go │ │ └── orm.go │ └── sql │ │ ├── README.md │ │ ├── conn.go │ │ ├── db.go │ │ ├── metrics.go │ │ ├── mysql.go │ │ ├── mysql_test.go │ │ ├── row.go │ │ ├── statement.go │ │ └── transaction.go ├── sync │ ├── errgroup │ │ ├── doc.go │ │ ├── errgroup.go │ │ ├── errgroup_test.go │ │ └── example_test.go │ └── pipeline │ │ └── fanout │ │ ├── example_test.go │ │ ├── fanout.go │ │ └── fanout_test.go ├── time │ ├── time.go │ └── time_test.go ├── trace │ ├── README.md │ ├── config.go │ ├── errors.go │ ├── jaeger │ │ └── jaeger.go │ ├── options.go │ ├── plugins │ │ └── function │ │ │ └── tracer.go │ └── tracer.go ├── transport │ ├── consumer │ │ ├── rabbitmq │ │ │ └── server.go │ │ └── redis │ │ │ ├── options.go │ │ │ └── server.go │ ├── crontab │ │ ├── logger.go │ │ └── server.go │ ├── grpc │ │ ├── client.go │ │ ├── clientoptions.go │ │ ├── codec.go │ │ ├── interceptor.go │ │ ├── metrics.go │ │ ├── resolver │ │ │ ├── direct │ │ │ │ ├── builder.go │ │ │ │ └── resolver.go │ │ │ └── discovery │ │ │ │ ├── builder.go │ │ │ │ └── resolver.go │ │ ├── server.go │ │ └── server_test.go │ ├── http │ │ ├── options.go │ │ ├── server.go │ │ ├── server_test.go │ │ └── status │ │ │ ├── status.go │ │ │ └── status_test.go │ └── transport.go ├── utils │ ├── README.md │ ├── debug.go │ ├── debug_test.go │ ├── host.go │ ├── host_test.go │ ├── ip.go │ ├── ip_test.go │ ├── pagination.go │ ├── slice.go │ ├── slice_test.go │ ├── string.go │ ├── time.go │ ├── url.go │ ├── utils.go │ ├── utils_test.go │ └── valid.go └── version │ ├── base.go │ ├── doc.go │ └── version.go ├── scripts ├── README.md ├── admin.sh └── wrktest.sh └── test ├── database.sql ├── docker-compose.yaml ├── kafka-docker-compose.yaml ├── nats-docker-compose.yaml ├── rabbitmq-docker-compose.yaml └── rabbitmq-isolated.conf /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | # required for all workflows 22 | security-events: write 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | # Override automatic language detection by changing the below list 28 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 29 | # TODO: Enable for javascript later 30 | language: ["go"] 31 | 32 | steps: 33 | - name: Checkout repo 34 | uses: actions/checkout@v3 35 | 36 | # Initializes the CodeQL tools for scanning. 37 | - name: Initialize CodeQL 38 | uses: github/codeql-action/init@v2 39 | with: 40 | languages: ${{ matrix.language }} 41 | 42 | - name: Perform CodeQL Analysis 43 | uses: github/codeql-action/analyze@v2 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: GoRelease 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Release on GitHub 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Git fetch 22 | run: git fetch --force --tags 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: 1.22 28 | 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v6 31 | with: 32 | distribution: goreleaser 33 | version: "~> v2" 34 | args: release --clean 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # logs 7 | # log 8 | logs 9 | 10 | # Editors 11 | *~ 12 | .*.swp 13 | .#* 14 | \#*# 15 | 16 | # Folders 17 | _obj 18 | _test 19 | 20 | # Architecture specific extensions/prefixes 21 | *.[568vq] 22 | [568vq].out 23 | 24 | *.cgo1.go 25 | *.cgo2.c 26 | _cgo_defun.c 27 | _cgo_gotypes.go 28 | _cgo_export.* 29 | 30 | _testmain.go 31 | 32 | *.exe 33 | *.test 34 | *.prof 35 | *.out 36 | coverage.txt 37 | cover.out 38 | .idea 39 | .idea/* 40 | bin/ 41 | .DS_Store 42 | 43 | vendor 44 | 45 | # custom 46 | eagle 47 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # reference 2 | # 需要安装:https://pre-commit.com/ 3 | # - https://blog.xizhibei.me/2019/08/12/the-outpost-of-project-quality-git-hooks/ 4 | # - https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks 5 | # - https://githooks.com/ 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v3.4.0 8 | hooks: 9 | - id: check-yaml 10 | - repo: git://github.com/dnephin/pre-commit-golang 11 | rev: master 12 | hooks: 13 | - id: go-fmt 14 | - id: go-vet 15 | - id: go-unit-tests 16 | - id: go-build 17 | #- repo: local 18 | # hooks: 19 | # - id: generate-docs 20 | # name: Generate docs 21 | # pass_filenames: false 22 | # entry: make docs 23 | # files: \.go$ 24 | # language: system 25 | # types: [text] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 1024casts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cmd/eagle/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-eagle/eagle/cmd/eagle 2 | 3 | go 1.22 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/AlecAivazis/survey/v2 v2.2.12 8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 9 | github.com/emicklei/proto v1.9.1 10 | github.com/fatih/color v1.9.0 11 | github.com/jedib0t/go-pretty/v6 v6.2.7 12 | github.com/spf13/cobra v1.1.3 13 | golang.org/x/mod v0.17.0 14 | ) 15 | 16 | require ( 17 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 18 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 19 | github.com/mattn/go-colorable v0.1.6 // indirect 20 | github.com/mattn/go-isatty v0.0.12 // indirect 21 | github.com/mattn/go-runewidth v0.0.13 // indirect 22 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 23 | github.com/rivo/uniseg v0.2.0 // indirect 24 | github.com/spf13/pflag v1.0.5 // indirect 25 | golang.org/x/crypto v0.35.0 // indirect 26 | golang.org/x/sys v0.30.0 // indirect 27 | golang.org/x/term v0.29.0 // indirect 28 | golang.org/x/text v0.22.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /cmd/eagle/internal/base/get.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.16 2 | // +build !go1.16 3 | 4 | package base 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | // GoInstall go get path. 13 | func GoInstall(path ...string) error { 14 | for _, p := range path { 15 | fmt.Printf("go get -u %s\n", p) 16 | cmd := exec.Command("go", "get", "-u", p) 17 | cmd.Stdout = os.Stdout 18 | cmd.Stderr = os.Stderr 19 | if err := cmd.Run(); err != nil { 20 | return err 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /cmd/eagle/internal/base/install.go: -------------------------------------------------------------------------------- 1 | //go:build go1.16 2 | // +build go1.16 3 | 4 | package base 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | // GoInstall go get path. 14 | func GoInstall(path ...string) error { 15 | for _, p := range path { 16 | if !strings.Contains(p, "@") { 17 | p += "@latest" 18 | } 19 | fmt.Printf("go install %s\n", p) 20 | cmd := exec.Command("go", "install", p) 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | if err := cmd.Run(); err != nil { 24 | return err 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /cmd/eagle/internal/base/mod_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "testing" 4 | 5 | func TestModuleVersion(t *testing.T) { 6 | v, err := ModuleVersion("golang.org/x/mod") 7 | if err != nil { 8 | t.Fatal(err) 9 | } 10 | t.Log(v) 11 | } 12 | 13 | func TestEagleMod(t *testing.T) { 14 | out := EagleMod() 15 | t.Log(out) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/eagle/internal/base/repo_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestRepo(t *testing.T) { 9 | r := NewRepo("https://github.com/go-eagle/eagle-layout.git", "main") 10 | if err := r.Clone(context.Background()); err != nil { 11 | t.Fatal(err) 12 | } 13 | if err := r.CopyTo(context.Background(), "/tmp/test_eagle_repo", "github.com/go-eagle/eagle-layout", nil); err != nil { 14 | t.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/eagle/internal/cache/add/add.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // CmdCache represents the new command. 13 | var CmdAdd = &cobra.Command{ 14 | Use: "add", 15 | Short: "Create a cache file by template", 16 | Long: "Create a cache file using the cache template. Example: eagle cache add UserCache", 17 | Run: run, 18 | } 19 | 20 | var ( 21 | targetDir string 22 | ) 23 | 24 | func init() { 25 | CmdAdd.Flags().StringVarP(&targetDir, "-target-dir", "t", "internal/dal/cache", "generate target directory") 26 | } 27 | 28 | func run(cmd *cobra.Command, args []string) { 29 | if len(args) == 0 { 30 | fmt.Println("Please enter the cache filename") 31 | return 32 | } 33 | // eg: eagle cache UserCache 34 | filename := args[0] 35 | 36 | c := &Cache{ 37 | Name: utils.Ucfirst(filename), // 首字母大写 38 | LcName: utils.Lcfirst(filename), // 首字母小写 39 | UsName: utils.Camel2Case(filename), // 下划线分隔 40 | ColonName: strings.ReplaceAll(utils.Camel2Case(filename), "_", ":"), // 冒号分隔 41 | Path: targetDir, 42 | ModName: utils.ModName(), 43 | } 44 | if err := c.Generate(); err != nil { 45 | fmt.Println(err) 46 | return 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cmd/eagle/internal/cache/add/cache.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 9 | ) 10 | 11 | // Cache is a cache generator. 12 | type Cache struct { 13 | Name string 14 | LcName string 15 | UsName string 16 | ColonName string 17 | Path string 18 | Service string 19 | Package string 20 | ModName string 21 | } 22 | 23 | // Generate generate a cache template. 24 | func (c *Cache) Generate() error { 25 | body, err := c.execute() 26 | if err != nil { 27 | return err 28 | } 29 | wd, err := os.Getwd() 30 | if err != nil { 31 | panic(err) 32 | } 33 | to := path.Join(wd, c.Path) 34 | if _, err := os.Stat(to); os.IsNotExist(err) { 35 | if err := os.MkdirAll(to, 0700); err != nil { 36 | return err 37 | } 38 | } 39 | name := path.Join(to, utils.Camel2Case(c.Name)+"_cache.go") 40 | if _, err := os.Stat(name); !os.IsNotExist(err) { 41 | return fmt.Errorf("%s already exists", c.Name) 42 | } 43 | return os.WriteFile(name, body, 0644) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/eagle/internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/cmd/eagle/internal/cache/add" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // CmdProto represents the proto command. 9 | var CmdCache = &cobra.Command{ 10 | Use: "cache", 11 | Short: "Generate the cache file", 12 | Long: "Generate the cache file.", 13 | Run: run, 14 | } 15 | 16 | func init() { 17 | CmdCache.AddCommand(add.CmdAdd) 18 | } 19 | 20 | func run(cmd *cobra.Command, args []string) { 21 | } 22 | -------------------------------------------------------------------------------- /cmd/eagle/internal/handler/add/add.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 9 | ) 10 | 11 | // CmdCache represents the new command. 12 | var CmdAdd = &cobra.Command{ 13 | Use: "add", 14 | Short: "Create a handler file by template", 15 | Long: "Create a handler file using the handler template. Example: eagle handler add demo", 16 | Run: run, 17 | } 18 | 19 | var ( 20 | targetDir string 21 | version string 22 | method string 23 | ) 24 | 25 | func init() { 26 | CmdAdd.Flags().StringVarP(&targetDir, "target-dir", "t", "internal/handler", "generate target directory") 27 | CmdAdd.Flags().StringVarP(&version, "version", "v", "v1", "handler version") 28 | CmdAdd.Flags().StringVarP(&method, "method", "m", "GET", "http method, eg: GET, POST, PUT, PATCH, DELETE") 29 | } 30 | 31 | func run(cmd *cobra.Command, args []string) { 32 | if len(args) == 0 { 33 | fmt.Println("Please enter the handler filename") 34 | return 35 | } 36 | // eg: eagle handler add demo 37 | filename := args[0] 38 | 39 | c := &Handler{ 40 | Name: utils.Ucfirst(filename), // 首字母大写 41 | LcName: utils.Lcfirst(filename), // 首字母小写 42 | UsName: utils.Camel2Case(filename), // 下划线分隔 43 | Path: targetDir, 44 | Version: version, 45 | Method: method, 46 | } 47 | if err := c.Generate(); err != nil { 48 | fmt.Println(err) 49 | return 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmd/eagle/internal/handler/add/handler.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 9 | ) 10 | 11 | // Handler is a cache generator. 12 | type Handler struct { 13 | Name string 14 | LcName string 15 | UsName string 16 | Path string 17 | Version string 18 | Method string 19 | } 20 | 21 | // Generate generate a Handler template. 22 | func (h *Handler) Generate() error { 23 | body, err := h.execute() 24 | if err != nil { 25 | return err 26 | } 27 | wd, err := os.Getwd() 28 | if err != nil { 29 | panic(err) 30 | } 31 | to := path.Join(wd, h.Path, h.Version) 32 | if _, err := os.Stat(to); os.IsNotExist(err) { 33 | if err := os.MkdirAll(to, 0700); err != nil { 34 | return err 35 | } 36 | } 37 | name := path.Join(to, utils.Camel2Case(h.Name)+".go") 38 | if _, err := os.Stat(name); !os.IsNotExist(err) { 39 | return fmt.Errorf("%s already exists", h.Name) 40 | } 41 | return os.WriteFile(name, body, 0644) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/eagle/internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/cmd/eagle/internal/handler/add" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // CmdHandler represents the handler command. 9 | var CmdHandler = &cobra.Command{ 10 | Use: "handler", 11 | Short: "Generate the handler file", 12 | Long: "Generate the handler/controller file.", 13 | Run: run, 14 | } 15 | 16 | func init() { 17 | CmdHandler.AddCommand(add.CmdAdd) 18 | } 19 | 20 | func run(cmd *cobra.Command, args []string) { 21 | } 22 | -------------------------------------------------------------------------------- /cmd/eagle/internal/proto/add/proto.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | ) 8 | 9 | // Proto define a proto generator. 10 | type Proto struct { 11 | Name string 12 | Path string 13 | Package string 14 | Service string 15 | GoPackage string 16 | JavaPackage string 17 | } 18 | 19 | // Generate generate a proto template. 20 | func (p *Proto) Generate() error { 21 | body, err := p.execute() 22 | if err != nil { 23 | return err 24 | } 25 | wd, err := os.Getwd() 26 | if err != nil { 27 | panic(err) 28 | } 29 | to := path.Join(wd, p.Path) 30 | if _, err := os.Stat(to); os.IsNotExist(err) { 31 | if err := os.MkdirAll(to, 0700); err != nil { 32 | return err 33 | } 34 | } 35 | name := path.Join(to, p.Name) 36 | if _, err := os.Stat(name); !os.IsNotExist(err) { 37 | return fmt.Errorf("%s already exists", p.Name) 38 | } 39 | return os.WriteFile(name, body, 0644) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/eagle/internal/proto/add/template.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "strings" 7 | ) 8 | 9 | const protoTemplate = ` 10 | syntax = "proto3"; 11 | 12 | package {{.Package}}; 13 | 14 | option go_package = "{{.GoPackage}}"; 15 | option java_multiple_files = true; 16 | option java_package = "{{.JavaPackage}}"; 17 | 18 | service {{.Service}}Service { 19 | rpc Create{{.Service}} (Create{{.Service}}Request) returns (Create{{.Service}}Reply); 20 | rpc Update{{.Service}} (Update{{.Service}}Request) returns (Update{{.Service}}Reply); 21 | rpc Delete{{.Service}} (Delete{{.Service}}Request) returns (Delete{{.Service}}Reply); 22 | rpc Get{{.Service}} (Get{{.Service}}Request) returns (Get{{.Service}}Reply); 23 | rpc List{{.Service}} (List{{.Service}}Request) returns (List{{.Service}}Reply); 24 | } 25 | 26 | message Create{{.Service}}Request {} 27 | message Create{{.Service}}Reply {} 28 | 29 | message Update{{.Service}}Request {} 30 | message Update{{.Service}}Reply {} 31 | 32 | message Delete{{.Service}}Request {} 33 | message Delete{{.Service}}Reply {} 34 | 35 | message Get{{.Service}}Request {} 36 | message Get{{.Service}}Reply {} 37 | 38 | message List{{.Service}}Request {} 39 | message List{{.Service}}Reply {} 40 | ` 41 | 42 | func (p *Proto) execute() ([]byte, error) { 43 | buf := new(bytes.Buffer) 44 | tmpl, err := template.New("proto").Parse(strings.TrimSpace(protoTemplate)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if err := tmpl.Execute(buf, p); err != nil { 49 | return nil, err 50 | } 51 | return buf.Bytes(), nil 52 | } 53 | -------------------------------------------------------------------------------- /cmd/eagle/internal/proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/go-eagle/eagle/cmd/eagle/internal/proto/add" 7 | "github.com/go-eagle/eagle/cmd/eagle/internal/proto/client" 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/proto/server" 9 | ) 10 | 11 | // CmdProto represents the proto command. 12 | var CmdProto = &cobra.Command{ 13 | Use: "proto", 14 | Short: "Generate the proto files", 15 | Long: "Generate the proto files.", 16 | Run: run, 17 | } 18 | 19 | func init() { 20 | CmdProto.AddCommand(add.CmdAdd) 21 | CmdProto.AddCommand(client.CmdClient) 22 | CmdProto.AddCommand(server.CmdServer) 23 | } 24 | 25 | func run(cmd *cobra.Command, args []string) { 26 | } 27 | -------------------------------------------------------------------------------- /cmd/eagle/internal/repo/add/add.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // CmdAdd represents the new command. 12 | var CmdAdd = &cobra.Command{ 13 | Use: "add", 14 | Short: "Create a repo file by template", 15 | Long: "Create a repo file using the repo template. Example: eagle repo add UserCache", 16 | Run: run, 17 | } 18 | 19 | var ( 20 | targetDir string 21 | withCache bool 22 | ) 23 | 24 | func init() { 25 | CmdAdd.Flags().StringVarP(&targetDir, "-target-dir", "t", "internal/repository", "generate target directory") 26 | CmdAdd.Flags().BoolVarP(&withCache, "-with-cache", "c", true, "add cache operate") 27 | } 28 | 29 | func run(cmd *cobra.Command, args []string) { 30 | if len(args) == 0 { 31 | fmt.Println("Please enter the repo filename") 32 | return 33 | } 34 | // eg: eagle cache UserCache 35 | filename := args[0] 36 | 37 | c := &Repo{ 38 | Name: utils.Ucfirst(filename), // 首字母大写 39 | LcName: utils.Lcfirst(filename), // 首字母小写 40 | UsName: utils.Camel2Case(filename), // 下划线分隔 41 | Path: targetDir, 42 | ModName: utils.ModName(), 43 | WithCache: withCache, 44 | } 45 | if err := c.Generate(); err != nil { 46 | fmt.Println(err) 47 | return 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/eagle/internal/repo/add/repo.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 9 | ) 10 | 11 | // Repo is a cache generator. 12 | type Repo struct { 13 | Name string 14 | LcName string 15 | UsName string 16 | Path string 17 | Service string 18 | Package string 19 | ModName string 20 | WithCache bool 21 | } 22 | 23 | // Generate generate a repo template. 24 | func (r *Repo) Generate() error { 25 | body, err := r.execute() 26 | if err != nil { 27 | return err 28 | } 29 | wd, err := os.Getwd() 30 | if err != nil { 31 | panic(err) 32 | } 33 | to := path.Join(wd, r.Path) 34 | if _, err := os.Stat(to); os.IsNotExist(err) { 35 | if err := os.MkdirAll(to, 0700); err != nil { 36 | return err 37 | } 38 | } 39 | name := path.Join(to, utils.Camel2Case(r.Name)+"_repo.go") 40 | if _, err := os.Stat(name); !os.IsNotExist(err) { 41 | return fmt.Errorf("%s already exists", r.Name) 42 | } 43 | return os.WriteFile(name, body, 0644) 44 | } 45 | -------------------------------------------------------------------------------- /cmd/eagle/internal/repo/repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/go-eagle/eagle/cmd/eagle/internal/repo/add" 7 | ) 8 | 9 | // CmdProto represents the proto command. 10 | var CmdRepo = &cobra.Command{ 11 | Use: "repo", 12 | Short: "Generate the repo file", 13 | Long: "Generate the repo file.", 14 | Run: run, 15 | } 16 | 17 | func init() { 18 | CmdRepo.AddCommand(add.CmdAdd) 19 | } 20 | 21 | func run(cmd *cobra.Command, args []string) { 22 | } 23 | -------------------------------------------------------------------------------- /cmd/eagle/internal/service/add/add.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // CmdAdd represents the new command. 12 | var CmdAdd = &cobra.Command{ 13 | Use: "add", 14 | Short: "Create a svc file by template", 15 | Long: "Create a svc file using the svc template. Example: eagle svc add User", 16 | Run: run, 17 | } 18 | 19 | var ( 20 | targetDir string 21 | ) 22 | 23 | func init() { 24 | CmdAdd.Flags().StringVarP(&targetDir, "-target-dir", "t", "internal/service", "generate target directory") 25 | } 26 | 27 | func run(cmd *cobra.Command, args []string) { 28 | if len(args) == 0 { 29 | fmt.Println("Please enter the cache filename") 30 | return 31 | } 32 | // eg: eagle service add User 33 | filename := args[0] 34 | 35 | c := &Service{ 36 | Name: utils.Ucfirst(filename), // 首字母大写 37 | LcName: utils.Lcfirst(filename), 38 | Path: targetDir, 39 | ModName: utils.ModName(), 40 | } 41 | if err := c.Generate(); err != nil { 42 | fmt.Println(err) 43 | return 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/eagle/internal/service/add/service.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 9 | ) 10 | 11 | // Service is a svc generator. 12 | type Service struct { 13 | Name string 14 | LcName string 15 | Path string 16 | Service string 17 | Package string 18 | ModName string 19 | } 20 | 21 | // Generate generate a svc template. 22 | func (s *Service) Generate() error { 23 | body, err := s.execute() 24 | if err != nil { 25 | return err 26 | } 27 | wd, err := os.Getwd() 28 | if err != nil { 29 | panic(err) 30 | } 31 | to := path.Join(wd, s.Path) 32 | if _, err := os.Stat(to); os.IsNotExist(err) { 33 | if err := os.MkdirAll(to, 0700); err != nil { 34 | return err 35 | } 36 | } 37 | name := path.Join(to, utils.Camel2Case(s.Name)+"_svc.go") 38 | if _, err := os.Stat(name); !os.IsNotExist(err) { 39 | return fmt.Errorf("%s already exists", s.Name) 40 | } 41 | return os.WriteFile(name, body, 0644) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/eagle/internal/service/add/template.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/alecthomas/template" 8 | ) 9 | 10 | var svcTemplate = ` 11 | package service 12 | 13 | import ( 14 | "context" 15 | 16 | "{{.ModName}}/internal/repository" 17 | ) 18 | 19 | // {{.Name}}Service define a interface 20 | type {{.Name}}Service interface { 21 | Hello(ctx context.Context) error 22 | } 23 | 24 | type {{.LcName}}Service struct { 25 | repo repository.{{.Name}}Repo 26 | } 27 | 28 | var _ {{.Name}}Service = (*{{.LcName}}Service)(nil) 29 | 30 | func New{{.Name}}Service(repo repository.{{.Name}}Repo) {{.Name}}Service { 31 | return &{{.LcName}}Service{ 32 | repo: repo, 33 | } 34 | } 35 | 36 | // Hello . 37 | func (s *{{.LcName}}Service) Hello(ctx context.Context) error { 38 | return nil 39 | } 40 | ` 41 | 42 | func (s *Service) execute() ([]byte, error) { 43 | buf := new(bytes.Buffer) 44 | tmpl, err := template.New("service").Parse(strings.TrimSpace(svcTemplate)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if err := tmpl.Execute(buf, s); err != nil { 49 | return nil, err 50 | } 51 | return buf.Bytes(), nil 52 | } 53 | -------------------------------------------------------------------------------- /cmd/eagle/internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/go-eagle/eagle/cmd/eagle/internal/service/add" 7 | ) 8 | 9 | // CmdService represents the service command. 10 | var CmdService = &cobra.Command{ 11 | Use: "svc", 12 | Aliases: []string{"service"}, 13 | Short: "Generate the service/svc file", 14 | Long: "Generate the service/svc file.", 15 | Run: run, 16 | } 17 | 18 | func init() { 19 | CmdService.AddCommand(add.CmdAdd) 20 | } 21 | 22 | func run(cmd *cobra.Command, args []string) { 23 | } 24 | -------------------------------------------------------------------------------- /cmd/eagle/internal/task/add/add.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | 9 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // CmdAdd represents the new command. 15 | var CmdAdd = &cobra.Command{ 16 | Use: "add", 17 | Short: "Create a task file by template", 18 | Long: "Create a task file using the task template. Example: eagle task add EmailWelcome", 19 | Run: run, 20 | } 21 | 22 | var ( 23 | targetDir string 24 | ) 25 | 26 | func init() { 27 | CmdAdd.Flags().StringVarP(&targetDir, "-target-dir", "t", "internal/tasks", "generate target directory") 28 | } 29 | 30 | func run(cmd *cobra.Command, args []string) { 31 | if len(args) == 0 { 32 | fmt.Printf("Please enter the %s\n", color.RedString("task name")) 33 | return 34 | } 35 | // eg: eagle task add EmailWelcome 36 | filename := args[0] 37 | 38 | c := &Task{ 39 | Name: utils.Ucfirst(filename), // 首字母大写 40 | LcName: utils.Lcfirst(filename), // 首字母小写 41 | UsName: utils.Camel2Case(filename), // 下划线分隔 42 | ColonName: strings.ReplaceAll(utils.Camel2Case(filename), "_", ":"), // 冒号分隔 43 | Path: targetDir, 44 | ModName: utils.ModName(), 45 | } 46 | if err := c.Generate(); err != nil { 47 | fmt.Println(err) 48 | return 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/eagle/internal/task/add/task.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | 9 | "github.com/go-eagle/eagle/cmd/eagle/internal/utils" 10 | ) 11 | 12 | // Repo is a cache generator. 13 | type Task struct { 14 | Name string 15 | LcName string 16 | UsName string 17 | ColonName string 18 | Path string 19 | Service string 20 | Package string 21 | ModName string 22 | WithCache bool 23 | } 24 | 25 | // Generate generate a repo template. 26 | func (t *Task) Generate() error { 27 | body, err := t.execute() 28 | if err != nil { 29 | return err 30 | } 31 | wd, err := os.Getwd() 32 | if err != nil { 33 | panic(err) 34 | } 35 | to := path.Join(wd, t.Path) 36 | if _, err := os.Stat(to); os.IsNotExist(err) { 37 | if err := os.MkdirAll(to, 0700); err != nil { 38 | return err 39 | } 40 | } 41 | name := path.Join(to, utils.Camel2Case(t.Name)+".go") 42 | if _, err := os.Stat(name); !os.IsNotExist(err) { 43 | return fmt.Errorf("%s already exists", t.Name) 44 | } 45 | return ioutil.WriteFile(name, body, 0644) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/eagle/internal/task/task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/cmd/eagle/internal/task/list" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/go-eagle/eagle/cmd/eagle/internal/task/add" 8 | ) 9 | 10 | // CmdProto represents the proto command. 11 | var CmdTask = &cobra.Command{ 12 | Use: "task", 13 | Short: "Generate the task file", 14 | Long: "Generate the task file.", 15 | Run: run, 16 | } 17 | 18 | func init() { 19 | CmdTask.AddCommand(add.CmdAdd) 20 | CmdTask.AddCommand(list.CmdList) 21 | } 22 | 23 | func run(cmd *cobra.Command, args []string) { 24 | } 25 | -------------------------------------------------------------------------------- /cmd/eagle/internal/upgrade/upgrade.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/base" 9 | ) 10 | 11 | // CmdUpgrade represents the upgrade command. 12 | var CmdUpgrade = &cobra.Command{ 13 | Use: "upgrade", 14 | Short: "Upgrade the eagle tools", 15 | Long: "Upgrade the eagle tools. Example: eagle upgrade", 16 | Run: Run, 17 | } 18 | 19 | // Run upgrade the eagle tools. 20 | func Run(cmd *cobra.Command, args []string) { 21 | err := base.GoInstall( 22 | "github.com/go-eagle/eagle/cmd/eagle", 23 | "github.com/go-eagle/eagle/cmd/protoc-gen-go-gin", 24 | "google.golang.org/protobuf/cmd/protoc-gen-go", 25 | "google.golang.org/grpc/cmd/protoc-gen-go-grpc", 26 | "github.com/envoyproxy/protoc-gen-validate", 27 | "github.com/google/gnostic", 28 | "github.com/google/gnostic/cmd/protoc-gen-openapi", 29 | "github.com/google/wire/cmd/wire", 30 | ) 31 | if err != nil { 32 | fmt.Println(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/eagle/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/go-eagle/eagle/cmd/eagle/internal/cache" 9 | "github.com/go-eagle/eagle/cmd/eagle/internal/handler" 10 | "github.com/go-eagle/eagle/cmd/eagle/internal/model" 11 | "github.com/go-eagle/eagle/cmd/eagle/internal/project" 12 | "github.com/go-eagle/eagle/cmd/eagle/internal/proto" 13 | "github.com/go-eagle/eagle/cmd/eagle/internal/repo" 14 | "github.com/go-eagle/eagle/cmd/eagle/internal/run" 15 | "github.com/go-eagle/eagle/cmd/eagle/internal/service" 16 | "github.com/go-eagle/eagle/cmd/eagle/internal/task" 17 | "github.com/go-eagle/eagle/cmd/eagle/internal/upgrade" 18 | ) 19 | 20 | var ( 21 | // Version is the version of the compiled software. 22 | Version = "v1.0.3" 23 | 24 | rootCmd = &cobra.Command{ 25 | Use: "eagle", 26 | Short: "Eagle: A microservice framework for Go", 27 | Long: `Eagle: A microservice framework for Go`, 28 | Version: Version, 29 | } 30 | ) 31 | 32 | func init() { 33 | rootCmd.AddCommand(project.CmdNew) 34 | rootCmd.AddCommand(run.CmdRun) 35 | rootCmd.AddCommand(handler.CmdHandler) 36 | rootCmd.AddCommand(cache.CmdCache) 37 | rootCmd.AddCommand(repo.CmdRepo) 38 | rootCmd.AddCommand(service.CmdService) 39 | rootCmd.AddCommand(proto.CmdProto) 40 | rootCmd.AddCommand(task.CmdTask) 41 | rootCmd.AddCommand(model.CmdNew) 42 | rootCmd.AddCommand(upgrade.CmdUpgrade) 43 | } 44 | 45 | func main() { 46 | if err := rootCmd.Execute(); err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-gin/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-eagle/eagle/cmd/protoc-gen-go-gin 2 | 3 | go 1.16 4 | 5 | require ( 6 | google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb 7 | google.golang.org/protobuf v1.27.1 8 | ) 9 | -------------------------------------------------------------------------------- /cmd/protoc-gen-go-gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "google.golang.org/protobuf/compiler/protogen" 8 | "google.golang.org/protobuf/types/pluginpb" 9 | ) 10 | 11 | const ( 12 | version = "0.0.14" 13 | ) 14 | 15 | var ( 16 | showVersion = flag.Bool("version", false, "print the version and exit") 17 | ) 18 | 19 | func main() { 20 | flag.Parse() 21 | if *showVersion { 22 | fmt.Printf("protoc-gen-go-gin %v\n", version) 23 | return 24 | } 25 | protogen.Options{ 26 | ParamFunc: flag.CommandLine.Set, 27 | }.Run(func(gen *protogen.Plugin) error { 28 | gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) 29 | for _, f := range gen.Files { 30 | if !f.Generate { 31 | continue 32 | } 33 | generateFile(gen, f) 34 | } 35 | return nil 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /config/docker/README.md: -------------------------------------------------------------------------------- 1 | # conf 2 | 3 | 默认从 config.local.yaml 加载配置, 可以通过下面命令生成: 4 | 5 | ```bash 6 | cp config.sample.yaml config.local.yaml 7 | ``` 8 | 9 | 如果是多环境可以设定不同的配置文件,比如: 10 | - config.local.yaml 本地开发环境 11 | - config.dev.yaml 开发环境 12 | - config.test.yaml 测试环境 13 | - config.pre.yaml 预发布环境 14 | - config.prod.yaml 线上环境 15 | 16 | 生成开发环境配置文件: 17 | 18 | ```bash 19 | cp config.sample.yaml config.dev.yaml 20 | ``` 21 | 22 | 以上配置是可以提交到Git等代码仓库的,`config.local.yaml` 配置文件默认不会被提交。 23 | 24 | 使用本地配置文件来运行程序,命令如下: 25 | 26 | ```bash 27 | # 本地启动 28 | # 也可以直接 ./eagle 29 | ./eagle -c config/config.local.yaml 30 | # 开发环境启动 31 | ./eagle -c config/config.dev.yaml 32 | # 线上环境启动 33 | ./eagle -c config/config.prod.yaml 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /config/docker/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /config/docker/database.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Driver: mysql # 驱动名称,目前支持 mysql,postgres,默认: mysql 3 | Name: eagle # 数据库名称 4 | Addr: db:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306 5 | UserName: root 6 | Password: root 7 | ShowLog: true # 是否打印所有SQL日志 8 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 9 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 10 | Timeout: 3s # 数据库连接超时时间 11 | ReadTimeout: 3s # 数据库去读超时时间, 0代表不限制 12 | WriteTimeout: 3s # 数据库写入超时时间, 0代表不限制 13 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 14 | SlowThreshold: 500ms # 慢查询阈值,设置后只打印慢查询日志,默认为200ms 15 | EnableTrace: false 16 | -------------------------------------------------------------------------------- /config/docker/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | ServiceName: user-service 7 | Filename: eagle 8 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 9 | LoggerDir: /data/logs 10 | LogRollingPolicy: daily 11 | LogRotateDate: 1 12 | LogRotateSize: 1 13 | LogBackupCount: 7 14 | FlushInterval: 1s 15 | -------------------------------------------------------------------------------- /config/docker/nginx_api.conf: -------------------------------------------------------------------------------- 1 | upstream web { 2 | server app:8080; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; 8 | 9 | location / { 10 | # https://stackoverflow.com/questions/42720618/docker-nginx-stopped-emerg-11-host-not-found-in-upstream 11 | resolver 127.0.0.1; 12 | 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-Forwarded-Host $http_host; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | 18 | client_max_body_size 5m; 19 | 20 | proxy_pass http://web; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /config/docker/redis.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Addr: redis:6379 3 | Password: "" 4 | DB: 0 5 | MinIdleConn: 200 6 | DialTimeout: 60s 7 | ReadTimeout: 500ms 8 | WriteTimeout: 500ms 9 | PoolSize: 100 10 | PoolTimeout: 240s 11 | EnableTrace: true -------------------------------------------------------------------------------- /config/docker/trace.yaml: -------------------------------------------------------------------------------- 1 | ServiceName: "eagle" 2 | LocalAgentHostPort: "127.0.0.1:6831" 3 | CollectorEndpoint: "http://localhost:14268/api/traces" -------------------------------------------------------------------------------- /config/local/README.md: -------------------------------------------------------------------------------- 1 | # conf 2 | 3 | 默认从 config/{ENV} 加载配置, 可以通过下面命令生成: 4 | 5 | 如果是多环境可以设定不同的配置目录,比如: 6 | - config/local 本地开发环境 7 | - config/test 测试环境 8 | - config/staging 预发布环境 9 | - config/prod 线上环境 10 | 11 | > 环境命名参考自:https://cloud.google.com/apis/design/directory_structure 12 | 13 | 生成测试环境配置文件: 14 | 15 | ```bash 16 | cp -r config/localhost config/test 17 | ``` 18 | 19 | 使用本地配置文件来运行程序,命令如下: 20 | 21 | ```bash 22 | # 本地启动 23 | # 也可以直接 APP_ENV={env} ./eagle 24 | APP_ENV=local ./eagle -c config 25 | 或 26 | ./eagle -e local -c config 27 | ``` 28 | -------------------------------------------------------------------------------- /config/local/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /config/local/database.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Driver: mysql # 驱动名称,目前支持: mysql,postgres,默认: mysql 3 | Name: eagle # 数据库名称 4 | Addr: localhost:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306, pg:5432 5 | UserName: root 6 | Password: 123456 7 | ShowLog: true # 是否打印所有SQL日志 8 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 9 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 10 | Timeout: 3s # 数据库连接超时时间, 如果是 PostgreSQL 不需要加入单位 11 | ReadTimeout: 3s # 数据库去读超时时间, 0代表不限制,如果是PostgreSQL, 3000代表3s 12 | WriteTimeout: 3s # 数据库写入超时时间, 0代表不限制,如果是PostgreSQL, 不会使用该字段的值 13 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 14 | SlowThreshold: 500ms # 慢查询阈值,设置后只打印慢查询日志,默认为200ms 15 | EnableTrace: false 16 | user: 17 | Driver: mysql # 驱动名称,目前支持: mysql,postgres,默认: mysql 18 | Name: eagle # 数据库名称 19 | Addr: localhost:3306 # 如果是 docker,可以替换为 对应的服务名称,eg: db:3306, pg:5432 20 | UserName: root 21 | Password: 123456 22 | ShowLog: true # 是否打印所有SQL日志 23 | MaxIdleConn: 10 # 最大闲置的连接数,0意味着使用默认的大小2, 小于0表示不使用连接池 24 | MaxOpenConn: 60 # 最大打开的连接数, 需要小于数据库配置中的max_connections数 25 | Timeout: 3s # 数据库连接超时时间 26 | ReadTimeout: 3s # 数据库去读超时时间, 0代表不限制 27 | WriteTimeout: 3s # 数据库写入超时时间, 0代表不限制 28 | ConnMaxLifeTime: 4h # 单个连接最大存活时间,建议设置比数据库超时时长(wait_timeout)稍小一些 29 | SlowThreshold: 500ms # 慢查询阈值,设置后只打印慢查询日志,默认为200ms 30 | EnableTrace: false 31 | -------------------------------------------------------------------------------- /config/local/kafka.yaml: -------------------------------------------------------------------------------- 1 | default: # 实例名称 2 | Version: "2.8.1" 3 | RequiredAcks: 1 4 | Topic: "test-topic" 5 | ConsumeTopic: 6 | - "test-topic1" 7 | - "test-topic2" 8 | Brokers: 9 | - "localhost:9092" 10 | GroupID: "test-group" 11 | Partitioner: "hash" 12 | 13 | order: # 另一个实例配置 14 | Version: "2.8.1" 15 | RequiredAcks: 1 16 | Topic: "order-topic" 17 | ConsumeTopic: 18 | - "order-topic" 19 | Brokers: 20 | - "localhost:9092" 21 | GroupID: "order-group" 22 | Partitioner: "random" 23 | -------------------------------------------------------------------------------- /config/local/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | ServiceName: user-service 7 | Filename: eagle 8 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 9 | LoggerDir: /data/logs 10 | LogRollingPolicy: daily 11 | LogRotateDate: 1 12 | LogRotateSize: 1 13 | LogBackupCount: 7 14 | FlushInterval: 1s 15 | -------------------------------------------------------------------------------- /config/local/nginx_api.conf: -------------------------------------------------------------------------------- 1 | upstream web { 2 | server app:8080; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; 8 | 9 | location / { 10 | # https://stackoverflow.com/questions/42720618/docker-nginx-stopped-emerg-11-host-not-found-in-upstream 11 | resolver 127.0.0.1; 12 | 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-Forwarded-Host $http_host; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | 18 | client_max_body_size 5m; 19 | 20 | proxy_pass http://web; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /config/local/rabbitmq.yaml: -------------------------------------------------------------------------------- 1 | test-demo: 2 | Uri: "amqp://guest:guest@localhost:5672/" 3 | AutoDeclare: true 4 | Timeout: 5s 5 | Exchange: 6 | Name: local-test-exchange 7 | Kind: direct 8 | Durable: true 9 | AutoDelete: false 10 | Internal: false 11 | NoWait: false 12 | Args: {} 13 | Queue: 14 | name: local-test-queue 15 | durable: true 16 | AutoDelete: false 17 | Exclusive: false 18 | NoWait: false 19 | Args: {} 20 | Bind: 21 | RoutingKey: local-test-routing-key 22 | NoWait: false 23 | Args: {} -------------------------------------------------------------------------------- /config/local/redis.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | Addr: 127.0.0.1:6379 3 | Password: "" 4 | DB: 0 5 | MinIdleConn: 200 6 | DialTimeout: 60s 7 | ReadTimeout: 500ms 8 | WriteTimeout: 500ms 9 | PoolSize: 100 10 | PoolTimeout: 240s 11 | EnableTrace: true -------------------------------------------------------------------------------- /config/local/trace.yaml: -------------------------------------------------------------------------------- 1 | ServiceName: "eagle" 2 | LocalAgentHostPort: "127.0.0.1:6831" 3 | CollectorEndpoint: "http://localhost:14268/api/traces" -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | ## 部署 2 | 3 | 主要存放一些部署相关的配置文件和脚本 4 | 5 | ### 部署Go应用 6 | 7 | > see: https://eddycjy.com/posts/kubernetes/2020-05-03-deployment/ 8 | 9 | 10 | ## 监控 11 | 12 | 包含对机器(node或者container)的监控、应用的监控、数据库的监控等。 13 | 14 | 使用 `docker-compose` 可以在本地一键部署,配置如下: 15 | 16 | ```yaml 17 | 18 | ``` 19 | 20 | ## 配置etcd 21 | 22 | create a namespace 23 | 24 | `$ kubectl create namespace etcd` 25 | 26 | create the service 27 | 28 | `$ kubectl apply -f etcd-service.yaml -n etcd` 29 | 30 | create the cluster(statefulSet) 31 | 32 | `$ cat etcd.yml.tmpl | etcd_config.bash | kubectl apply -n etcd -f -` 33 | 34 | Verify the cluster's health 35 | 36 | `$ kubectl exec -it etcd-0 -n etcd etcdctl cluster-health` 37 | 38 | The cluster is exposed through minikube's IP 39 | 40 | ```bash 41 | $ IP=$(minikube ip) 42 | $ PORT=$(kubectl get services -o jsonpath="{.spec.ports[].nodePort}" etcd-client -n etcd) 43 | $ etcdctl --endpoints http://${IP}:${PORT} get foo 44 | ``` 45 | 46 | Destroy the services 47 | 48 | ```bash 49 | $ kubectl delete services,statefulsets --all -n etcd 50 | ``` 51 | 52 | 给Etcd集群做个Web UI 53 | 54 | ``` 55 | $ kubectl apply -f etcd-ui-configmap.yaml 56 | $ kubectl apply -f etcd-ui.yaml 57 | ``` 58 | 59 | > Ref: https://github.com/kevinyan815/LearningKubernetes/tree/master/e3w 60 | 61 | > 参考: 62 | > https://mp.weixin.qq.com/s/AkIvkW22dvqcdFXkiTpv8Q 63 | > https://github.com/kevinyan815/LearningKubernetes/tree/master/etcd -------------------------------------------------------------------------------- /deploy/grafana/alertmanager.yml: -------------------------------------------------------------------------------- 1 | #global config 2 | global: 3 | resolve_timeout: 30s 4 | slack_api_url: '' 5 | route: 6 | receiver: 'slack-notifications' 7 | 8 | receivers: 9 | - name: 'slack-notifications' 10 | slack_configs: 11 | - channel: '#alerts' 12 | send_resolved: true -------------------------------------------------------------------------------- /deploy/grafana/rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: Rules 3 | rules: 4 | - alert: InstanceDown 5 | # Condition for alerting 6 | expr: up == 0 7 | for: 1m 8 | # Annotation - additional informational labels to store more information 9 | annotations: 10 | title: 'Instance {{ $labels.instance }} down' 11 | description: '{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute.' 12 | # Labels - additional labels to be attached to the alert 13 | labels: 14 | severity: 'critical' -------------------------------------------------------------------------------- /deploy/k8s/aws-auth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: aws-auth 5 | namespace: kube-system 6 | data: 7 | # userarn 需要修改为自己的 8 | mapUsers: | 9 | - userarn: arn:aws:iam::111222333:user/github-ci 10 | username: github-ci 11 | groups: 12 | - system:masters -------------------------------------------------------------------------------- /deploy/k8s/consul-server-http.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: consul-server-http 5 | spec: 6 | selector: 7 | name: consul-server 8 | type: NodePort 9 | ports: 10 | - protocol: TCP 11 | port: 8500 12 | targetPort: 8500 13 | nodePort: 30098 14 | name: consul-server-tcp -------------------------------------------------------------------------------- /deploy/k8s/consul-server-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: consul-server 5 | labels: 6 | name: consul-server 7 | spec: 8 | selector: 9 | name: consul-server 10 | ports: 11 | - name: http 12 | port: 8500 13 | targetPort: 8500 14 | - name: https 15 | port: 8443 16 | targetPort: 8443 17 | - name: rpc 18 | port: 8400 19 | targetPort: 8400 -------------------------------------------------------------------------------- /deploy/k8s/etcd-service.yaml: -------------------------------------------------------------------------------- 1 | # https://sgotti.me/post/kubernetes-persistent-etcd/ 2 | --- 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: etcd 7 | namespace: etcd 8 | annotations: 9 | # Create endpoints also if the related pod isn't ready 10 | service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" 11 | spec: 12 | ports: 13 | - port: 2379 14 | name: client 15 | - port: 2380 16 | name: peer 17 | clusterIP: None 18 | selector: 19 | component: etcd 20 | --- 21 | apiVersion: v1 22 | kind: Service 23 | metadata: 24 | name: etcd-client 25 | namespace: etcd 26 | spec: 27 | ports: 28 | - name: http 29 | nodePort: 30453 30 | port: 2379 31 | targetPort: 2379 32 | protocol: TCP 33 | type: NodePort 34 | selector: 35 | component: etcd 36 | ... 37 | -------------------------------------------------------------------------------- /deploy/k8s/etcd-ui-configmap.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: e3w-configmap 6 | namespace: etcd 7 | labels: 8 | config: e3w.ini 9 | data: 10 | e3w-config.default.ini: | 11 | [app] 12 | port=8080 13 | auth=false 14 | [etcd] 15 | root_key=root 16 | dir_value= 17 | addr=etcd-0.etcd.etcd.svc.cluster.local:2379,etcd-1.etcd.etcd.svc.cluster.local:2379,etcd-2.etcd.etcd.svc.cluster.local:2379 18 | username= 19 | password= 20 | cert_file= 21 | key_file= 22 | ca_file= -------------------------------------------------------------------------------- /deploy/k8s/etcd-ui-deployment-service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: e3w-deployment 6 | namespace: etcd 7 | labels: 8 | app: e3w 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: etcd-client-e3w 14 | template: 15 | metadata: 16 | labels: 17 | app: etcd-client-e3w 18 | spec: 19 | containers: 20 | - name: e3w-app-container 21 | image: soyking/e3w:latest 22 | ports: 23 | - name: e3w-server-port 24 | containerPort: 8080 25 | volumeMounts: 26 | - name: e3w-configmap-volume 27 | mountPath: /app/conf/config.default.ini 28 | subPath: config.default.ini 29 | volumes: 30 | - name: e3w-configmap-volume 31 | configMap: 32 | name: e3w-configmap 33 | items: 34 | - key: e3w-config.default.ini 35 | path: config.default.ini 36 | --- 37 | kind: Service 38 | apiVersion: v1 39 | metadata: 40 | name: e3w-service 41 | namespace: etcd 42 | spec: 43 | type: NodePort 44 | selector: 45 | app: etcd-client-e3w 46 | ports: 47 | - protocol: TCP 48 | targetPort: e3w-server-port 49 | nodePort: 30081 50 | port: 80 -------------------------------------------------------------------------------- /deploy/k8s/etcd-ui.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: e3w-deployment 6 | namespace: etcd 7 | labels: 8 | app: e3w 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: etcd-client-e3w 14 | template: 15 | metadata: 16 | labels: 17 | app: etcd-client-e3w 18 | spec: 19 | containers: 20 | - name: e3w-app-container 21 | image: soyking/e3w:latest 22 | ports: 23 | - name: e3w-server-port 24 | containerPort: 8080 25 | --- 26 | kind: Service 27 | apiVersion: v1 28 | metadata: 29 | name: e3w-service 30 | namespace: etcd 31 | spec: 32 | type: NodePort 33 | selector: 34 | app: etcd-client-e3w 35 | ports: 36 | - protocol: TCP 37 | targetPort: e3w-server-port 38 | nodePort: 30081 39 | port: 80 -------------------------------------------------------------------------------- /deploy/k8s/etcd_config.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | MINIKUBE_IP=$(minikube ip) 5 | MINIKUBE_PORT=$(kubectl get services -o jsonpath="{.spec.ports[].nodePort}" etcd-client -n etcd) 6 | 7 | cat - |\ 8 | sed 's/\$MINIKUBE_IP'"/$MINIKUBE_IP/g" |\ 9 | sed 's/\$MINIKUBE_PORT'"/$MINIKUBE_PORT/g" -------------------------------------------------------------------------------- /deploy/k8s/go-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: go-app-deployment 5 | labels: 6 | app: go-app 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: go-app 12 | strategy: 13 | type: RollingUpdate 14 | rollingUpdate: 15 | maxSurge: 1 16 | maxUnavailable: 1 17 | template: # Pod 模板的定义 18 | metadata: 19 | labels: 20 | app: go-app 21 | spec: # Pod里容器相关的定义 22 | containers: 23 | - name: go-app 24 | image: 456002253389.dkr.ecr.us-east-1.amazonaws.com/microservices:latest 25 | resources: 26 | limits: 27 | memory: "128Mi" 28 | cpu: "100m" # 0.1核,1000m = 1核心 29 | ports: 30 | - containerPort: 8080 31 | # - containerPort: 9090 32 | # readinessProbe: # 就绪探针 33 | # exec: 34 | # command: [ "/bin/grpc_health_probe", "-addr=:9090" ] 35 | # initialDelaySeconds: 5 36 | # livenessProbe: # 存活探针 37 | # exec: 38 | # command: [ "/bin/grpc_health_probe", "-addr=:9090" ] 39 | # initialDelaySeconds: 10 40 | -------------------------------------------------------------------------------- /deploy/k8s/go-ingress.yaml: -------------------------------------------------------------------------------- 1 | # Ingress Class 2 | apiVersion: networking.k8s.io/v1 3 | kind: IngressClass 4 | metadata: 5 | name: nginx 6 | spec: 7 | controller: k8s.io/ingress-nginx 8 | --- 9 | 10 | apiVersion: networking.k8s.io/v1 11 | kind: Ingress 12 | metadata: 13 | name: go-app-ingress 14 | annotations: 15 | nginx.ingress.kubernetes.io/use-regex: "true" 16 | spec: 17 | ingressClassName: nginx 18 | rules: 19 | - host: api.go-eagle.org 20 | http: 21 | paths: 22 | - pathType: Prefix 23 | path: "/" 24 | backend: 25 | service: 26 | name: go-app-svc 27 | port: 28 | number: 80 29 | # tls: 30 | # - hosts: 31 | # - api.go-app.org 32 | # secretName: go-app-cert -------------------------------------------------------------------------------- /deploy/k8s/go-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: go-app-svc 5 | labels: 6 | app: go-app 7 | spec: 8 | selector: 9 | app: go-app 10 | ports: 11 | - protocol: TCP 12 | port: 80 13 | targetPort: 8080 14 | type: LoadBalancer -------------------------------------------------------------------------------- /deploy/prometheus/README.md: -------------------------------------------------------------------------------- 1 | 2 | 通过启动 prometheus 来采集服务的各种运行指标,方便监控。 3 | 4 | 启动一个 prometheus 5 | 6 | ```bash 7 | prometheus --config.file=prometheus.yml 8 | ``` 9 | 10 | 可以通过 http://localhost:8090/ping 来检测服务的运行情况 11 | 12 | 13 | ## Reference 14 | 15 | - https://github.com/yolossn/Prometheus-Basics -------------------------------------------------------------------------------- /deploy/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # 抓取数据的间隔,默认为1分钟 3 | evaluation_interval: 15s # 评估数据的间隔 4 | external_labels: 5 | monitor: 'my-monitor' 6 | 7 | # Alertmanager configuration 8 | #alerting: 9 | # alertmanagers: 10 | # - static_configs: 11 | # - targets: 12 | # - localhost:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | #rule_files: 16 | # - ./rules/rules.yml 17 | 18 | scrape_configs: 19 | # prometheus 自身数据的抓取配置 20 | - job_name: prometheus 21 | static_configs: 22 | - targets: ["localhost:9090"] 23 | - job_name: node_exporter 24 | scrape_interval: 10s 25 | static_configs: 26 | - targets: ["localhost:9100","node_exporter:9100"] 27 | 28 | # Go 程序数据的抓取配置 29 | - job_name: go_app_server 30 | scrape_interval: 10s # 抓取间隔 31 | metrics_path: /metrics # 抓取数据的URL路径 32 | # scheme defaults to 'http' 33 | static_configs: 34 | - targets: ["localhost:8080","app:8080"] 35 | labels: 36 | instance: go-eagle 37 | -------------------------------------------------------------------------------- /docs/images/clean-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-eagle/eagle/1b5c80f19311299eb51c4f8c4f1dba7adf0e2210/docs/images/clean-arch.png -------------------------------------------------------------------------------- /docs/images/eagle-layout-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-eagle/eagle/1b5c80f19311299eb51c4f8c4f1dba7adf0e2210/docs/images/eagle-layout-arch.png -------------------------------------------------------------------------------- /examples/config/config.yaml: -------------------------------------------------------------------------------- 1 | name: eagle 2 | version: 1.0.0 -------------------------------------------------------------------------------- /examples/config/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/go-eagle/eagle/pkg/config" 8 | ) 9 | 10 | func main() { 11 | type Service struct { 12 | Name string 13 | Version string 14 | } 15 | 16 | c := config.New(".") 17 | 18 | var svc Service 19 | err := c.Load("config", &svc) 20 | if err != nil { 21 | log.Fatalf("load config err: %+v", err) 22 | } 23 | fmt.Println("service name: ", svc.Name) 24 | 25 | conf, err := c.LoadWithType("server", "yaml") 26 | if err != nil { 27 | log.Fatalf("load server err: %+v", err) 28 | } 29 | fmt.Println("http addr: ", conf.GetString("http.addr")) 30 | fmt.Println("http port: ", conf.GetInt("http.port")) 31 | } 32 | -------------------------------------------------------------------------------- /examples/config/server.yaml: -------------------------------------------------------------------------------- 1 | HTTP: 2 | addr: 127.0.0.1 3 | port: 8000 -------------------------------------------------------------------------------- /examples/crontab/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /examples/crontab/crontab.yaml: -------------------------------------------------------------------------------- 1 | Timezone: "Asia/Shanghai" 2 | Tasks: 3 | - Name: send_email 4 | Schedule: "* * * * *" 5 | - Name: greet 6 | Schedule: "@every 1s" 7 | -------------------------------------------------------------------------------- /examples/crontab/jobs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/log" 5 | ) 6 | 7 | type GreetingJob struct { 8 | Name string 9 | } 10 | 11 | func (g GreetingJob) Run() { 12 | log.Info("Hello ", g.Name) 13 | } 14 | 15 | type SendEmail struct { 16 | Name string 17 | } 18 | 19 | func (g SendEmail) Run() { 20 | log.Info("Send mail to... ", g.Name) 21 | } 22 | -------------------------------------------------------------------------------- /examples/crontab/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: console # json or console 5 | Level: info # 日志级别,debug, info, warn, error 6 | Name: eagle 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerDir: /tmp/logs 9 | LogRollingPolicy: daily 10 | LogRotateDate: 1 11 | LogRotateSize: 1 12 | LogBackupCount: 7 13 | -------------------------------------------------------------------------------- /examples/crontab/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | eagle "github.com/go-eagle/eagle/pkg/app" 5 | "github.com/go-eagle/eagle/pkg/config" 6 | logger "github.com/go-eagle/eagle/pkg/log" 7 | "github.com/go-eagle/eagle/pkg/transport/crontab" 8 | "github.com/robfig/cron/v3" 9 | ) 10 | 11 | // cd examples/crontab/ 12 | // go run server.go jobs.go 13 | func main() { 14 | // init config 15 | c := config.New(".") 16 | 17 | var cfg eagle.Config 18 | if err := c.Load("app", &cfg); err != nil { 19 | panic(err) 20 | } 21 | // set global 22 | eagle.Conf = &cfg 23 | 24 | // init jobs 25 | jobs := map[string]cron.Job{ 26 | "greet": GreetingJob{"golang"}, 27 | "send_email": SendEmail{" user1"}, 28 | } 29 | 30 | log := logger.Init(logger.WithFilename("crontab")) 31 | srv := crontab.NewServer(jobs, crontab.Logger{Log: log}) 32 | 33 | // start app 34 | app := eagle.New( 35 | eagle.WithName(cfg.Name), 36 | eagle.WithVersion(cfg.Version), 37 | eagle.WithLogger(log), 38 | eagle.WithServer( 39 | srv, 40 | ), 41 | ) 42 | 43 | if err := app.Run(); err != nil { 44 | panic(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/helloworld/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "google.golang.org/grpc" 11 | 12 | pb "github.com/go-eagle/eagle/examples/helloworld/helloworld" 13 | ) 14 | 15 | const ( 16 | defaultName = "eagle" 17 | ) 18 | 19 | var ( 20 | addr = flag.String("addr", "localhost:9090", "the address to connect to") 21 | name = flag.String("name", defaultName, "Name to greet") 22 | ) 23 | 24 | func main() { 25 | conn, err := grpc.Dial(*addr, grpc.WithInsecure(), grpc.WithBlock()) 26 | if err != nil { 27 | fmt.Printf("grpc dial err: %v", err) 28 | panic("grpc dial err") 29 | } 30 | defer func() { 31 | _ = conn.Close() 32 | }() 33 | 34 | cli := pb.NewGreeterClient(conn) 35 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 36 | defer cancel() 37 | 38 | req := &pb.HelloRequest{ 39 | Name: *name, 40 | } 41 | reply, err := cli.SayHello(ctx, req) 42 | if err != nil { 43 | log.Fatalf("could not greet: %v", err) 44 | } 45 | fmt.Printf("Greeting : %s", reply.GetMessage()) 46 | } 47 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld/greeter.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package helloworld; 4 | 5 | option go_package="github.com/go-eagle/eagle/examples/helloworld/helloworld"; 6 | 7 | // The greeting service definition. 8 | service Greeter { 9 | // Sends a greeting 10 | rpc SayHello (HelloRequest) returns (HelloReply) {} 11 | } 12 | 13 | // The request message containing the user's name. 14 | message HelloRequest { 15 | string name = 1; 16 | } 17 | 18 | // The response message containing the greetings 19 | message HelloReply { 20 | string message = 1; 21 | } -------------------------------------------------------------------------------- /examples/helloworld/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | //"google.golang.org/grpc" 8 | 9 | "github.com/go-eagle/eagle/examples/helloworld/helloworld" 10 | pb "github.com/go-eagle/eagle/examples/helloworld/helloworld" 11 | grpcSrv "github.com/go-eagle/eagle/internal/server" 12 | "github.com/go-eagle/eagle/pkg/app" 13 | eagle "github.com/go-eagle/eagle/pkg/app" 14 | logger "github.com/go-eagle/eagle/pkg/log" 15 | ) 16 | 17 | // server is used to implement helloworld.GreeterServer. 18 | type server struct { 19 | pb.UnimplementedGreeterServer 20 | } 21 | 22 | // SayHello implements helloworld.GreeterServer 23 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 24 | if in.Name == "" { 25 | return nil, fmt.Errorf("invalid argument %s", in.Name) 26 | } 27 | return &pb.HelloReply{Message: fmt.Sprintf("Hello %+v", in.Name)}, nil 28 | } 29 | 30 | func main() { 31 | cfg := &app.ServerConfig{ 32 | Network: "tcp", 33 | Addr: ":9090", 34 | ReadTimeout: 200, 35 | WriteTimeout: 200, 36 | } 37 | 38 | grpcServer := grpcSrv.NewGRPCServer(cfg) 39 | srv := &server{} 40 | helloworld.RegisterGreeterServer(grpcServer, srv) 41 | 42 | // start app 43 | app := eagle.New( 44 | eagle.WithName("eagle"), 45 | eagle.WithVersion("v1.0.0"), 46 | eagle.WithLogger(logger.GetLogger()), 47 | eagle.WithServer( 48 | grpcServer, 49 | ), 50 | ) 51 | 52 | if err := app.Run(); err != nil { 53 | panic(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/log/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | ServiceName: demo-service 7 | Filename: demo-file 8 | Writers: file # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 9 | LoggerDir: /tmp/logs 10 | LogRollingPolicy: daily 11 | LogRotateDate: 1 12 | LogRotateSize: 1 13 | LogBackupCount: 7 14 | -------------------------------------------------------------------------------- /examples/log/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/config" 5 | "github.com/go-eagle/eagle/pkg/log" 6 | ) 7 | 8 | func main() { 9 | // load config 10 | _ = config.New(".") 11 | 12 | // print log using default filename 13 | log.Init() 14 | log.Info("test log") 15 | 16 | // print log using custom filename 17 | log.Init(log.WithFilename("custom-filename")) 18 | log.Info("test log with filename") 19 | 20 | // print log using custom dir and filename 21 | log.Init(log.WithFilename("user/custom")) 22 | log.Info("test log with dir and filename") 23 | } 24 | -------------------------------------------------------------------------------- /examples/queue/kafka/config/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /examples/queue/kafka/config/kafka.yaml: -------------------------------------------------------------------------------- 1 | default: # 实例名称 2 | Version: "2.8.1" 3 | RequiredAcks: 1 4 | Topic: "test-topic" 5 | ConsumeTopic: 6 | - "test-topic1" 7 | - "test-topic2" 8 | Brokers: 9 | - "localhost:9092" 10 | GroupID: "test-group" 11 | Partitioner: "hash" 12 | 13 | order: # 另一个实例配置 14 | Version: "2.8.1" 15 | RequiredAcks: 1 16 | Topic: "order-topic" 17 | ConsumeTopic: 18 | - "order-topic" 19 | Brokers: 20 | - "localhost:9092" 21 | GroupID: "order-group" 22 | Partitioner: "random" 23 | -------------------------------------------------------------------------------- /examples/queue/kafka/config/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: eagle 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /tmp/log/eagle.log 9 | LoggerWarnFile: /tmp/log/eagle.wf.log 10 | LoggerErrorFile: /tmp/log/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /examples/queue/kafka/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/go-eagle/eagle/pkg/queue/kafka" 8 | ) 9 | 10 | func main() { 11 | // 1. 初始化配置 12 | kafka.Load() 13 | defer kafka.Close() 14 | 15 | // 2. 获取配置信息(可选) 16 | configs := kafka.GetConfig() 17 | if len(configs) == 0 { 18 | log.Fatal("No kafka config found") 19 | } 20 | 21 | // 3. 使用配置进行消息发布 22 | ctx := context.Background() 23 | err := kafka.Publish(ctx, "default", "test-topic", "hello world") 24 | if err != nil { 25 | log.Printf("Failed to publish message: %v", err) 26 | } 27 | 28 | // 4. 使用配置进行消息消费 29 | handler := func(data []byte) error { 30 | log.Printf("Received message: %s", string(data)) 31 | return nil 32 | } 33 | 34 | // 从默认实例消费 35 | go func() { 36 | err := kafka.ConsumePartition(ctx, "default", "test-topic", handler) 37 | if err != nil { 38 | log.Printf("Failed to consume message: %v", err) 39 | } 40 | }() 41 | 42 | // 从order实例消费 43 | go func() { 44 | err := kafka.ConsumePartition(ctx, "order", "order-topic", handler) 45 | if err != nil { 46 | log.Printf("Failed to consume message: %v", err) 47 | } 48 | }() 49 | 50 | select {} 51 | } 52 | -------------------------------------------------------------------------------- /examples/queue/nats/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "log" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/go-eagle/eagle/pkg/queue/nats" 11 | ) 12 | 13 | func main() { 14 | var ( 15 | addr = "nats://localhost:4222" 16 | topic = "hello" 17 | ) 18 | producer := nats.NewProducer(addr) 19 | consumer := nats.NewConsumer(addr) 20 | 21 | published := make(chan struct{}) 22 | received := make(chan struct{}) 23 | wg := sync.WaitGroup{} 24 | wg.Add(2) 25 | go func() { 26 | for { 27 | select { 28 | case <-published: 29 | time.Sleep(3 * time.Second) 30 | if err := producer.Publish(topic, []byte("hello nats")); err != nil { 31 | log.Fatal(err) 32 | } 33 | log.Println("producer handler publish msg: ", "hello nats") 34 | 35 | case <-received: 36 | wg.Done() 37 | break 38 | } 39 | } 40 | }() 41 | go func() { 42 | for { 43 | // nolint: gosimple 44 | select { 45 | default: 46 | handler := func(message []byte) error { 47 | decodeMessage, _ := base64.StdEncoding.DecodeString(strings.Trim(string(message), "\"")) 48 | log.Println("consumer handler receive msg: ", string(decodeMessage)) 49 | received <- struct{}{} 50 | wg.Done() 51 | return nil 52 | } 53 | if err := consumer.Consume(topic, handler); err != nil { 54 | log.Fatal(err) 55 | } 56 | time.Sleep(5 * time.Second) 57 | } 58 | } 59 | }() 60 | 61 | published <- struct{}{} 62 | wg.Wait() 63 | } 64 | -------------------------------------------------------------------------------- /examples/queue/rabbitmq/consumer/config/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /examples/queue/rabbitmq/consumer/config/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: console # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: eagle 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /tmp/log/eagle.log 9 | LoggerWarnFile: /tmp/log/eagle.wf.log 10 | LoggerErrorFile: /tmp/log/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /examples/queue/rabbitmq/consumer/config/rabbitmq.yaml: -------------------------------------------------------------------------------- 1 | test-demo: 2 | URI: "amqp://guest:guest@localhost:5672/" 3 | AutoDeclare: true 4 | Timeout: 5s 5 | Exchange: 6 | Name: local-test-exchange 7 | Kind: direct 8 | Durable: true 9 | AutoDelete: false 10 | Internal: false 11 | NoWait: false 12 | Args: {} 13 | Queue: 14 | name: local-test-queue 15 | durable: true 16 | AutoDelete: false 17 | Exclusive: false 18 | NoWait: false 19 | Args: {} 20 | Bind: 21 | RoutingKey: local-test-routing-key 22 | NoWait: false 23 | Args: {} 24 | 25 | test-multi: 26 | URI: "amqp://guest:guest@localhost:5672/" 27 | AutoDeclare: true 28 | Timeout: 5s 29 | Exchange: 30 | Name: local-test2-exchange 31 | Kind: direct 32 | Durable: true 33 | AutoDelete: false 34 | Internal: false 35 | NoWait: false 36 | Args: {} 37 | Queue: 38 | name: local-test2-queue 39 | durable: true 40 | AutoDelete: false 41 | Exclusive: false 42 | NoWait: false 43 | Args: {} 44 | Bind: 45 | RoutingKey: local-test2-routing-key 46 | NoWait: false 47 | Args: {} -------------------------------------------------------------------------------- /examples/queue/rabbitmq/producer/config/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /examples/queue/rabbitmq/producer/config/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: json # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: eagle 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /tmp/log/eagle.log 9 | LoggerWarnFile: /tmp/log/eagle.wf.log 10 | LoggerErrorFile: /tmp/log/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /examples/queue/rabbitmq/producer/delay_publish.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "time" 8 | 9 | eagle "github.com/go-eagle/eagle/pkg/app" 10 | "github.com/go-eagle/eagle/pkg/config" 11 | logger "github.com/go-eagle/eagle/pkg/log" 12 | "github.com/go-eagle/eagle/pkg/queue/rabbitmq" 13 | "github.com/go-eagle/eagle/pkg/queue/rabbitmq/options" 14 | ) 15 | 16 | // 启动 rabbitmq 17 | // docker run -it --name rabbitmq -p 5672:5672 -p 15672:15672 -v $PWD/plugins:/plugins rabbitmq:3.10-management 18 | // 访问ui: http://127.0.0.1:15672/ 19 | // cd examples/queue/rabbitmq/producer 20 | // go run delay_publish.go 21 | func main() { 22 | c := config.New(*cfgDir, config.WithEnv(*env)) 23 | var cfg eagle.Config 24 | if err := c.Load("app", &cfg); err != nil { 25 | panic(err) 26 | } 27 | // set global 28 | eagle.Conf = &cfg 29 | 30 | logger.Init() 31 | 32 | rabbitmq.Load() 33 | defer rabbitmq.Close() 34 | 35 | opts := []options.PublishOption{ 36 | options.WithPublishOptionContentType("application/json"), 37 | } 38 | 39 | var message string 40 | for i := 0; i < 100000; i++ { 41 | message = "Hello World RabbitMQ!" + time.Now().String() 42 | msg := map[string]interface{}{ 43 | "message": message, 44 | } 45 | data, _ := json.Marshal(msg) 46 | if err := rabbitmq.PublishWithDelay(context.Background(), "test-demo", data, 10, opts...); err != nil { 47 | log.Fatalf("failed publish message: %s", err.Error()) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/queue/redis/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false 12 | EnableTrace: false 13 | EnablePprof: true 14 | 15 | HTTP: 16 | Addr: :8080 17 | ReadTimeout: 3s 18 | WriteTimeout: 3s 19 | GRPC: 20 | Addr: :9090 21 | ReadTimeout: 5s 22 | WriteTimeout: 5s -------------------------------------------------------------------------------- /examples/queue/redis/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/go-eagle/eagle/pkg/config" 8 | "github.com/hibiken/asynq" 9 | ) 10 | 11 | var ( 12 | client *asynq.Client 13 | once sync.Once 14 | ) 15 | 16 | type Config struct { 17 | Queue struct { 18 | Addr string 19 | Password string 20 | DB int 21 | MinIdleConn int 22 | DialTimeout time.Duration 23 | ReadTimeout time.Duration 24 | WriteTimeout time.Duration 25 | PoolSize int 26 | PoolTimeout time.Duration 27 | Concurrency int //并发数 28 | } `json:"redis"` 29 | } 30 | 31 | func GetClient() *asynq.Client { 32 | once.Do(func() { 33 | //c := config.New("config", config.WithEnv("local")) 34 | c := config.New(".") 35 | var cfg Config 36 | if err := c.Load("redis", &cfg); err != nil { 37 | panic(err) 38 | } 39 | client = asynq.NewClient(asynq.RedisClientOpt{ 40 | Addr: cfg.Queue.Addr, 41 | Password: cfg.Queue.Password, 42 | DB: cfg.Queue.DB, 43 | DialTimeout: cfg.Queue.DialTimeout, 44 | ReadTimeout: cfg.Queue.ReadTimeout, 45 | WriteTimeout: cfg.Queue.WriteTimeout, 46 | PoolSize: cfg.Queue.PoolSize, 47 | }) 48 | }) 49 | return client 50 | } 51 | -------------------------------------------------------------------------------- /examples/queue/redis/logger.yaml: -------------------------------------------------------------------------------- 1 | Development: false 2 | DisableCaller: false 3 | DisableStacktrace: false 4 | Encoding: console # json or console 5 | Level: info # 日志级别,INFO, WARN, ERROR 6 | Name: eagle 7 | Writers: console # 有2个可选项:file,console 选择file会将日志记录到logger_file指定的日志文件中,选择console会将日志输出到标准输出,当然也可以两者同时选择 8 | LoggerFile: /tmp/log/eagle.log 9 | LoggerWarnFile: /tmp/log/eagle.wf.log 10 | LoggerErrorFile: /tmp/log/eagle.err.log 11 | LogRollingPolicy: daily 12 | LogRotateDate: 1 13 | LogRotateSize: 1 14 | LogBackupCount: 7 -------------------------------------------------------------------------------- /examples/queue/redis/producer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | // cd examples/queue/redis/consumer/ 6 | // go run producer.go handler.go client.go 7 | func main() { 8 | for i := 0; i < 10; i++ { 9 | err := NewEmailWelcomeTask(EmailWelcomePayload{ 10 | UserID: time.Now().Unix(), 11 | }) 12 | if err != nil { 13 | panic(err) 14 | } 15 | time.Sleep(time.Second) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/queue/redis/redis.yaml: -------------------------------------------------------------------------------- 1 | Queue: 2 | Addr: 127.0.0.1:6379 3 | Password: "" 4 | DB: 0 5 | MinIdleConn: 200 6 | DialTimeout: 60s 7 | ReadTimeout: 500ms 8 | WriteTimeout: 500ms 9 | PoolSize: 100 10 | PoolTimeout: 240s 11 | EnableTrace: true 12 | -------------------------------------------------------------------------------- /examples/queue/redis/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | eagle "github.com/go-eagle/eagle/pkg/app" 5 | "github.com/go-eagle/eagle/pkg/config" 6 | logger "github.com/go-eagle/eagle/pkg/log" 7 | redisMQ "github.com/go-eagle/eagle/pkg/transport/consumer/redis" 8 | "github.com/hibiken/asynq" 9 | "github.com/spf13/pflag" 10 | ) 11 | 12 | // redis queue consumer 13 | // cd examples/queue/redis/consumer/ 14 | // go run server.go handler.go client.go 15 | func main() { 16 | pflag.Parse() 17 | 18 | // init config 19 | c := config.New(".") 20 | var cfg eagle.Config 21 | if err := c.Load("app", &cfg); err != nil { 22 | panic(err) 23 | } 24 | // set global 25 | eagle.Conf = &cfg 26 | 27 | logger.Init() 28 | 29 | srv := redisMQ.NewServer( 30 | asynq.RedisClientOpt{Addr: "localhost:6379"}, 31 | asynq.Config{ 32 | // Specify how many concurrent workers to use 33 | Concurrency: 10, 34 | // Optionally specify multiple queues with different priority. 35 | Queues: map[string]int{ 36 | redisMQ.QueueCritical: 6, 37 | redisMQ.QueueDefault: 3, 38 | redisMQ.QueueLow: 1, 39 | }, 40 | // See the godoc for other configuration options 41 | }, 42 | ) 43 | 44 | // register handler 45 | srv.RegisterHandler(TypeEmailWelcome, HandleEmailWelcomeTask) 46 | // here register other handlers... 47 | 48 | // start app 49 | app := eagle.New( 50 | eagle.WithName(cfg.Name), 51 | eagle.WithVersion(cfg.Version), 52 | eagle.WithLogger(logger.GetLogger()), 53 | eagle.WithServer( 54 | srv, 55 | ), 56 | ) 57 | 58 | if err := app.Run(); err != nil { 59 | panic(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/cache/cache_client.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-eagle/eagle/internal/model" 7 | "github.com/go-eagle/eagle/pkg/cache" 8 | "github.com/go-eagle/eagle/pkg/encoding" 9 | "github.com/go-eagle/eagle/pkg/redis" 10 | ) 11 | 12 | func getCacheClient(ctx context.Context) cache.Cache { 13 | jsonEncoding := encoding.JSONEncoding{} 14 | cachePrefix := "" 15 | client := cache.NewRedisCache(redis.RedisClient, cachePrefix, jsonEncoding, func() interface{} { 16 | return &model.UserBaseModel{} 17 | }) 18 | 19 | return client 20 | } 21 | -------------------------------------------------------------------------------- /internal/ecode/README.md: -------------------------------------------------------------------------------- 1 | # 业务错误码定义 2 | 3 | > 公共错误码已经在 `github.com/go-eagle/eagle/pkg/errno` 包中,可以直接使用 4 | 5 | 业务的错误码可以根据模块按文件进行定义 6 | 7 | 使用时公共错误码 以 `errno.开头`,业务错误码以 `ecode.开头` 8 | 9 | ## Demo 10 | 11 | ```go 12 | // 公共错误码 13 | import "github.com/go-eagle/eagle/pkg/errno" 14 | ... 15 | errno.InternalServerError 16 | 17 | // 业务错误码 18 | import "github.com/go-eagle/eagle/internal/ecode" 19 | ... 20 | ecode.ErrUserNotFound 21 | ``` -------------------------------------------------------------------------------- /internal/ecode/user.go: -------------------------------------------------------------------------------- 1 | package ecode 2 | 3 | import "github.com/go-eagle/eagle/pkg/errcode" 4 | 5 | //nolint: golint 6 | var ( 7 | // user errors 8 | ErrUserNotFound = errcode.NewError(20101, "The user was not found.") 9 | ErrPasswordIncorrect = errcode.NewError(20102, "账号或密码错误") 10 | ErrAreaCodeEmpty = errcode.NewError(20103, "手机区号不能为空") 11 | ErrPhoneEmpty = errcode.NewError(20104, "手机号不能为空") 12 | ErrGenVCode = errcode.NewError(20105, "生成验证码错误") 13 | ErrSendSMS = errcode.NewError(20106, "发送短信错误") 14 | ErrSendSMSTooMany = errcode.NewError(20107, "已超出当日限制,请明天再试") 15 | ErrVerifyCode = errcode.NewError(20108, "验证码错误") 16 | ErrEmailOrPassword = errcode.NewError(20109, "邮箱或密码错误") 17 | ErrTwicePasswordNotMatch = errcode.NewError(20110, "两次密码输入不一致") 18 | ErrRegisterFailed = errcode.NewError(20111, "注册失败") 19 | 20 | ErrCannotFollowSelf = errcode.NewError(20201, "不能关注自己") 21 | ) 22 | -------------------------------------------------------------------------------- /internal/handler/v1/user/get.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/spf13/cast" 8 | 9 | "github.com/go-eagle/eagle/internal/ecode" 10 | "github.com/go-eagle/eagle/internal/repository" 11 | "github.com/go-eagle/eagle/internal/service" 12 | "github.com/go-eagle/eagle/pkg/errcode" 13 | "github.com/go-eagle/eagle/pkg/log" 14 | ) 15 | 16 | // Get 获取用户信息 17 | // @Summary 通过用户id获取用户信息 18 | // @Description Get an user by user id 19 | // @Tags 用户 20 | // @Accept json 21 | // @Produce json 22 | // @Param id path string true "用户id" 23 | // @Success 200 {object} model.UserInfo "用户信息" 24 | // @Router /users/:id [get] 25 | func Get(c *gin.Context) { 26 | log.Info("Get function called.") 27 | 28 | userID := cast.ToUint64(c.Param("id")) 29 | if userID == 0 { 30 | response.Error(c, errcode.ErrInvalidParam) 31 | return 32 | } 33 | 34 | // Get the user by the `user_id` from the database. 35 | u, err := service.Svc.Users().GetUserByID(c.Request.Context(), userID) 36 | if errors.Is(err, repository.ErrNotFound) { 37 | log.Errorf("get user info err: %+v", err) 38 | response.Error(c, ecode.ErrUserNotFound) 39 | return 40 | } 41 | if err != nil { 42 | response.Error(c, errcode.ErrInternalServer.WithDetails(err.Error())) 43 | return 44 | } 45 | 46 | //time.Sleep(5 * time.Second) 47 | 48 | response.Success(c, u) 49 | } 50 | -------------------------------------------------------------------------------- /internal/handler/v1/user/register.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/go-eagle/eagle/internal/ecode" 7 | "github.com/go-eagle/eagle/internal/service" 8 | "github.com/go-eagle/eagle/pkg/errcode" 9 | "github.com/go-eagle/eagle/pkg/log" 10 | ) 11 | 12 | // Register 注册 13 | // @Summary 注册 14 | // @Description 用户注册 15 | // @Tags 用户 16 | // @Produce json 17 | // @Param req body RegisterRequest true "请求参数" 18 | // @Success 200 {object} model.UserInfo "用户信息" 19 | // @Router /Register [post] 20 | func Register(c *gin.Context) { 21 | // Binding the data with the u struct. 22 | var req RegisterRequest 23 | if err := c.ShouldBindJSON(&req); err != nil { 24 | log.Warnf("register bind param err: %v", err) 25 | response.Error(c, errcode.ErrInvalidParam) 26 | return 27 | } 28 | 29 | log.Infof("register req: %#v", req) 30 | // check param 31 | if req.Username == "" || req.Email == "" || req.Password == "" { 32 | log.Warnf("params is empty: %v", req) 33 | response.Error(c, errcode.ErrInvalidParam) 34 | return 35 | } 36 | 37 | // 两次密码是否正确 38 | if req.Password != req.ConfirmPassword { 39 | log.Warnf("twice password is not same") 40 | response.Error(c, ecode.ErrTwicePasswordNotMatch) 41 | return 42 | } 43 | 44 | err := service.Svc.Users().Register(c, req.Username, req.Email, req.Password) 45 | if err != nil { 46 | log.Warnf("register err: %v", err) 47 | response.Error(c, ecode.ErrRegisterFailed.WithDetails(err.Error())) 48 | return 49 | } 50 | 51 | response.Success(c, nil) 52 | } 53 | -------------------------------------------------------------------------------- /internal/handler/v1/user/update.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-eagle/eagle/internal/service" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/spf13/cast" 10 | 11 | "github.com/go-eagle/eagle/pkg/errcode" 12 | "github.com/go-eagle/eagle/pkg/log" 13 | ) 14 | 15 | // Update 更新用户信息 16 | // @Summary Update a user info by the user identifier 17 | // @Description Update a user by ID 18 | // @Tags 用户 19 | // @Accept json 20 | // @Produce json 21 | // @Param id path uint64 true "The user's database id index num" 22 | // @Param user body model.UserBaseModel true "The user info" 23 | // @Success 200 {object} app.Response "{"code":0,"message":"OK","data":null}" 24 | // @Router /users/{id} [put] 25 | func Update(c *gin.Context) { 26 | // Get the user id from the url parameter. 27 | userID := cast.ToUint64(c.Param("id")) 28 | 29 | // Binding the user data. 30 | var req UpdateRequest 31 | if err := c.Bind(&req); err != nil { 32 | log.Warnf("bind request param err: %+v", err) 33 | response.Error(c, errcode.ErrInvalidParam) 34 | return 35 | } 36 | log.Infof("user update req: %#v", req) 37 | 38 | userMap := make(map[string]interface{}) 39 | userMap["avatar"] = req.Avatar 40 | userMap["sex"] = req.Sex 41 | err := service.Svc.Users().UpdateUser(context.TODO(), userID, userMap) 42 | if err != nil { 43 | log.Warnf("[user] update user err, %v", err) 44 | response.Error(c, errcode.ErrInternalServer) 45 | return 46 | } 47 | 48 | response.Success(c, userID) 49 | } 50 | -------------------------------------------------------------------------------- /internal/handler/v1/user/vcode.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/pkg/errors" 6 | 7 | "github.com/go-eagle/eagle/internal/ecode" 8 | "github.com/go-eagle/eagle/internal/service" 9 | "github.com/go-eagle/eagle/pkg/log" 10 | ) 11 | 12 | // VCode 获取验证码 13 | // @Summary 根据手机号获取校验码 14 | // @Description Get an user by username 15 | // @Tags 用户 16 | // @Accept json 17 | // @Produce json 18 | // @Param area_code query string true "区域码,比如86" 19 | // @Param phone query string true "手机号" 20 | // @Success 200 {object} app.Response 21 | // @Router /vcode [get] 22 | func VCode(c *gin.Context) { 23 | // 验证区号和手机号是否为空 24 | if c.Query("area_code") == "" { 25 | log.Warn("vcode area code is empty") 26 | response.Error(c, ecode.ErrAreaCodeEmpty) 27 | return 28 | } 29 | 30 | phone := c.Query("phone") 31 | if phone == "" { 32 | log.Warn("vcode phone is empty") 33 | response.Error(c, ecode.ErrPhoneEmpty) 34 | return 35 | } 36 | 37 | // TODO: 频率控制,以防攻击 38 | 39 | // 生成短信验证码 40 | verifyCode, err := service.Svc.VCode().GenLoginVCode(phone) 41 | if err != nil { 42 | log.Warnf("gen login verify code err, %v", errors.WithStack(err)) 43 | response.Error(c, ecode.ErrGenVCode) 44 | return 45 | } 46 | 47 | // 发送短信 48 | err = service.Svc.SMS().SendSMS(phone, verifyCode) 49 | if err != nil { 50 | log.Warnf("send phone sms err, %v", errors.WithStack(err)) 51 | response.Error(c, ecode.ErrSendSMS) 52 | return 53 | } 54 | 55 | response.Success(c, nil) 56 | } 57 | -------------------------------------------------------------------------------- /internal/middleware/README.md: -------------------------------------------------------------------------------- 1 | # 中间件 2 | 3 | 业务自定义的中间可以放本目录 -------------------------------------------------------------------------------- /internal/middleware/translations.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | // nolint 4 | import ( 5 | en_translations "github.com/go-playground/validator/v10/translations/en" 6 | zh_translations "github.com/go-playground/validator/v10/translations/zh" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/gin-gonic/gin/binding" 10 | "github.com/go-playground/locales/en" 11 | "github.com/go-playground/locales/zh" 12 | "github.com/go-playground/locales/zh_Hant_TW" 13 | "github.com/go-playground/universal-translator" 14 | "github.com/go-playground/validator/v10" 15 | ) 16 | 17 | var uni = ut.New(en.New(), zh.New(), zh_Hant_TW.New()) 18 | 19 | // Translations . 20 | func Translations() gin.HandlerFunc { 21 | return func(c *gin.Context) { 22 | locale := c.GetHeader("locale") 23 | trans, _ := uni.GetTranslator(locale) 24 | v, ok := binding.Validator.Engine().(*validator.Validate) 25 | if ok { 26 | switch locale { 27 | case "zh": 28 | _ = zh_translations.RegisterDefaultTranslations(v, trans) 29 | case "en": 30 | _ = en_translations.RegisterDefaultTranslations(v, trans) 31 | default: 32 | _ = zh_translations.RegisterDefaultTranslations(v, trans) 33 | } 34 | c.Set("trans", trans) 35 | } 36 | 37 | c.Next() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/model/README.md: -------------------------------------------------------------------------------- 1 | # Model 2 | 3 | Model层,或者叫 `Entity`,实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致。 4 | 5 | 通过http访问返回的结构体也都放到这里,在输出前进行结构体的转换。一般 `XXXInfo` 的形式。 6 | 比如: 在返回终端用户前对 `userModel` 进行转换,转换为结构体 `UserInfo` 。 7 | 8 | ## 数据库约定 9 | 10 | 这里默认使用 `MySQL` 数据库,尽量使用 `InnoDB` 作为存储引擎。 11 | 12 | ### 相关表采用统一前缀 13 | 14 | 比如和用户相关的,使用 `user_` 作为表前缀: 15 | 16 | ```bash 17 | user_base // 用户基础表 18 | user_follow // 用户关注表 19 | user_fans // 用户粉丝表 20 | user_stat // 用户统计表 21 | ``` 22 | 23 | ### 统一字段名 24 | 25 | 一个表中需要包含的三大字段:主键(id),创建时间(created_at),更新时间(updated_at) 26 | 如果需要用户id,一般用 `user_id` 表示即可。 -------------------------------------------------------------------------------- /internal/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gorm.io/gorm" 7 | 8 | "github.com/go-eagle/eagle/pkg/storage/orm" 9 | ) 10 | 11 | const ( 12 | // DefaultDatabase default database 13 | DefaultDatabase = "default" 14 | // UserDatabase user database 15 | UserDatabase = "user" 16 | ) 17 | 18 | // Init 初始化数据库 19 | func Init() { 20 | err := orm.New( 21 | DefaultDatabase, 22 | UserDatabase, 23 | ) 24 | if err != nil { 25 | panic(fmt.Sprintf("new orm database err: %v", err)) 26 | } 27 | } 28 | 29 | // GetDB 返回默认的数据库 30 | func GetDB() (*gorm.DB, error) { 31 | return orm.GetDB(DefaultDatabase) 32 | } 33 | 34 | // GetUserDB 获取用户数据库实例 35 | func GetUserDB() (*gorm.DB, error) { 36 | return orm.GetDB(UserDatabase) 37 | } 38 | -------------------------------------------------------------------------------- /internal/model/user_fans.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // UserFansModel 粉丝表 6 | type UserFansModel struct { 7 | ID uint64 `gorm:"primary_key;AUTO_INCREMENT;column:id" json:"-"` 8 | FollowerUID uint64 `gorm:"column:follower_uid" json:"follower_uid"` 9 | Status int `gorm:"column:status" json:"status"` 10 | UserID uint64 `gorm:"column:user_id" json:"user_id"` 11 | CreatedAt time.Time `gorm:"column:created_at" json:"-"` 12 | UpdatedAt time.Time `gorm:"column:updated_at" json:"-"` 13 | } 14 | 15 | // TableName sets the insert table name for this struct type 16 | func (u *UserFansModel) TableName() string { 17 | return "user_fans" 18 | } 19 | -------------------------------------------------------------------------------- /internal/model/user_follow.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // UserFollowModel 关注表 6 | type UserFollowModel struct { 7 | ID uint64 `gorm:"primary_key;AUTO_INCREMENT;column:id" json:"-"` 8 | FollowedUID uint64 `gorm:"column:followed_uid" json:"followed_uid"` 9 | Status int `gorm:"column:status" json:"status"` 10 | UserID uint64 `gorm:"column:user_id" json:"user_id"` 11 | CreatedAt time.Time `gorm:"column:created_at" json:"-"` 12 | UpdatedAt time.Time `gorm:"column:updated_at" json:"-"` 13 | } 14 | 15 | // TableName sets the insert table name for this struct type 16 | func (u *UserFollowModel) TableName() string { 17 | return "user_follow" 18 | } 19 | -------------------------------------------------------------------------------- /internal/model/user_stat.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // UserStatModel 用户数据统计表 6 | type UserStatModel struct { 7 | ID uint64 `gorm:"primary_key;AUTO_INCREMENT;column:id" json:"-"` 8 | UserID uint64 `gorm:"column:user_id;not null" json:"user_id" binding:"required"` 9 | FollowCount int `gorm:"column:follow_count" json:"follow_count"` 10 | FollowerCount int `gorm:"column:follower_count" json:"follower_count"` 11 | Status int `gorm:"column:status" json:"status"` 12 | CreatedAt time.Time `gorm:"column:created_at" json:"-"` 13 | UpdatedAt time.Time `gorm:"column:updated_at" json:"-"` 14 | } 15 | 16 | // TableName 表名 17 | func (u *UserStatModel) TableName() string { 18 | return "user_stat" 19 | } 20 | -------------------------------------------------------------------------------- /internal/pkg/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 业务公共模块 3 | 4 | - 重复检测 5 | - counter 6 | - id生成器 -------------------------------------------------------------------------------- /internal/pkg/idalloc.go: -------------------------------------------------------------------------------- 1 | // Package pkg ID 分配器,主要使用redis进行分配 2 | package pkg 3 | 4 | import "github.com/go-eagle/eagle/pkg/redis" 5 | 6 | // IDAlloc define struct 7 | type IDAlloc struct { 8 | idGenerator *redis.IDAlloc 9 | } 10 | 11 | // NewIDAlloc create a id alloc 12 | func NewIDAlloc() *IDAlloc { 13 | return &IDAlloc{ 14 | idGenerator: redis.NewIDAlloc(redis.RedisClient), 15 | } 16 | } 17 | 18 | // GetUserID generate user id from redis 19 | func (i *IDAlloc) GetUserID() (int64, error) { 20 | return i.idGenerator.GetNewID("user_id", 1) 21 | } 22 | -------------------------------------------------------------------------------- /internal/pkg/repeat.go: -------------------------------------------------------------------------------- 1 | // Package pkg 重复提交模型封装 2 | package pkg 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-eagle/eagle/pkg/log" 9 | "github.com/go-eagle/eagle/pkg/redis" 10 | "github.com/go-eagle/eagle/pkg/utils" 11 | ) 12 | 13 | // CRepeat define struct 14 | type CRepeat struct { 15 | cRepeatClient redis.CheckRepeat 16 | } 17 | 18 | // NewCRepeat create a check repeat 19 | func NewCRepeat() *CRepeat { 20 | return &CRepeat{ 21 | cRepeatClient: redis.NewCheckRepeat(redis.RedisClient), 22 | } 23 | } 24 | 25 | // getKey return a check repeat key 26 | func (c *CRepeat) getKey(userID int64, check string) string { 27 | key, err := utils.Md5(fmt.Sprintf("%d:%s", userID, check)) 28 | if err != nil { 29 | log.Warnf("md5 string err: %v", err) 30 | } 31 | return key 32 | } 33 | 34 | // Set record a repeat value 35 | func (c *CRepeat) Set(userID int64, check string, value interface{}, expiration time.Duration) error { 36 | return c.cRepeatClient.Set(c.getKey(userID, check), value, expiration) 37 | } 38 | 39 | // SetNX set 40 | func (c *CRepeat) SetNX(userID int64, check string, value interface{}, expiration time.Duration) (bool, error) { 41 | return c.cRepeatClient.SetNX(c.getKey(userID, check), value, expiration) 42 | } 43 | 44 | // Get get value 45 | func (c *CRepeat) Get(userID int64, check string) (interface{}, error) { 46 | return c.cRepeatClient.Get(c.getKey(userID, check)) 47 | } 48 | 49 | // Del delete 50 | func (c *CRepeat) Del(userID int64, check string) int64 { 51 | return c.cRepeatClient.Del(c.getKey(userID, check)) 52 | } 53 | -------------------------------------------------------------------------------- /internal/repository/README.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | 3 | Repository,是数据访问层,负责访问 DB、MC、外部 HTTP 等接口,对上层屏蔽数据访问细节。 4 | 后续更换、升级ORM引擎,不影响业务逻辑。能提高测试效率,单元测试时,用Mock对象代替实际的数据库存取,可以成倍地提高测试用例运行速度。 5 | 应用 Repository 模式所带来的好处,远高于实现这个模式所增加的代码。只要项目分层,都应当使用这个模式。 6 | Repository是DDD中的概念,强调 Repository 是受Domain(本项目主要是Service)驱动的。 7 | 对 Model 层只能单表操作,每一个方法都有一个参数 `db *grom.DB` 实例,方便事务操作。 8 | 9 | 具体职责有: 10 | - SQL 拼接和 DB 访问逻辑 11 | - DB 的拆库分表逻辑 12 | - DB 的缓存读写逻辑 13 | - HTTP 接口调用逻辑 14 | 15 | > Tips: 如果是返回的列表,尽量返回map,方便service使用。 16 | 17 | 建议: 18 | - 推荐使用编写原生SQL 19 | - 禁止使用连表查询,好处是易扩展,比如分库分表 20 | - 逻辑部分在程序中进行处理 21 | 22 | 一个业务一个目录,每一个repo go文件对应一个表操作,比如用户是在user目录下,涉及用户相关的都可以放到这里, 23 | 根据不同的模块分离到不同的文件,同时又避免了单个文件func太多的问题。比如: 24 | - 用户基础服务- user_base_repo.go 25 | - 用户关注- user_follow_repo.go 26 | - 用户喜欢- user_like_repo.go 27 | - 用户评论- user_comment_repo.go 28 | 29 | ## 单元测试 30 | 31 | 关于数据库的单元测试可以用到的几个库: 32 | - go-sqlmock https://github.com/DATA-DOG/go-sqlmock 主要用来和数据库的交互操作:增删改 33 | - GoMock https://github.com/golang/mock 34 | 35 | ## Reference 36 | - https://github.com/realsangil/apimonitor/blob/fe1e9ef75dfbf021822d57ee242089167582934a/pkg/rsdb/repository.go 37 | - https://youtu.be/twcDf_Y2gXY?t=636 38 | - [Unit testing GORM with go-sqlmock in Go](https://medium.com/@rosaniline/unit-testing-gorm-with-go-sqlmock-in-go-93cbce1f6b5b) 39 | - [如何使用Sqlmock对GORM应用进行单元测试](https://1024casts.com/topics/R9re7QDaq8MnJoaXRZxdljbNA5BwoK) 40 | -------------------------------------------------------------------------------- /internal/server/grpc.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-eagle/eagle/pkg/app" 7 | "github.com/go-eagle/eagle/pkg/transport/grpc" 8 | ) 9 | 10 | // NewGRPCServer creates a gRPC server 11 | func NewGRPCServer(cfg *app.ServerConfig) *grpc.Server { 12 | grpcServer := grpc.NewServer( 13 | grpc.Network("tcp"), 14 | grpc.Address(":9090"), 15 | grpc.Timeout(3*time.Second), 16 | ) 17 | 18 | // register biz service 19 | // v1.RegisterUserServiceServer(grpcServer, service.Svc.Users()) 20 | 21 | return grpcServer 22 | } 23 | -------------------------------------------------------------------------------- /internal/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/internal/routers" 5 | "github.com/go-eagle/eagle/pkg/app" 6 | "github.com/go-eagle/eagle/pkg/transport/http" 7 | ) 8 | 9 | // NewHTTPServer creates a HTTP server 10 | func NewHTTPServer(c *app.ServerConfig) *http.Server { 11 | router := routers.NewRouter() 12 | 13 | srv := http.NewServer( 14 | http.WithAddress(c.Addr), 15 | http.WithReadTimeout(c.ReadTimeout), 16 | http.WithWriteTimeout(c.WriteTimeout), 17 | ) 18 | 19 | srv.Handler = router 20 | // NOTE: register svc to http server 21 | 22 | return srv 23 | } 24 | -------------------------------------------------------------------------------- /internal/service/README.md: -------------------------------------------------------------------------------- 1 | # service 2 | 3 | - 业务逻辑层,处于 `handler` 层和 `repository` 层之间 4 | - `service` 只能通过 `repository` 层获取数据 5 | - 面向接口编程 6 | - 依赖接口,不要依赖实现 7 | - 如果有事务处理,在这一层进行处理 8 | - 如果是调用的第三方服务,请不要加 `cache`, 避免缓存不一致(对方更新数据,这边无法知晓) 9 | - 由于 `service` 会被 `http` 或 `rpc` 调用,默认会提供 `http` 调用的,比如:`GetUserInfo()`, 10 | 如果 `rpc` 需要调用,可以对 `GetUserInfo()` 进行一层封装, 比如:`GetUser()`。 11 | 12 | ## Reference 13 | 14 | - https://github.com/qiangxue/go-rest-api 15 | - https://github.com/irahardianto/service-pattern-go 16 | - https://github.com/golang-standards/project-layout 17 | - https://www.youtube.com/watch?v=oL6JBUk6tj0 18 | - https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html 19 | - https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1 20 | - [按需写 service 服务层逻辑](https://www.5-wow.com/article/detail/89) 21 | - [Go 编程实战:如何组织代码、编写测试?](https://www.infoq.cn/article/4TAWp8YNYcVD4t046EGd) 22 | - https://github.com/sdgmf/go-project-sample 23 | - [Golang微服务最佳实践](https://sdgmf.github.io/goproject/) 24 | - [layout 常见大型 Web 项目分层](https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-07-layout-of-web-project.html) 25 | - [在 Golang 中尝试简洁架构](https://studygolang.com/articles/12909) 26 | -------------------------------------------------------------------------------- /internal/service/common_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // GetUserID 返回用户id 6 | func GetUserID(c *gin.Context) uint64 { 7 | if c == nil { 8 | return 0 9 | } 10 | 11 | // uid 必须和 middleware/auth 中的 uid 命名一致 12 | if v, exists := c.Get("uid"); exists { 13 | uid, ok := v.(uint64) 14 | if !ok { 15 | return 0 16 | } 17 | 18 | return uid 19 | } 20 | return 0 21 | } 22 | -------------------------------------------------------------------------------- /internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/internal/repository" 5 | ) 6 | 7 | // Svc global var 8 | var Svc Service 9 | 10 | const ( 11 | // DefaultLimit 默认分页数 12 | DefaultLimit = 50 13 | 14 | // MaxID 最大id 15 | MaxID = 0xffffffffffff 16 | 17 | // DefaultAvatar 默认头像 key 18 | DefaultAvatar = "default_avatar.png" 19 | ) 20 | 21 | // Service define all service 22 | type Service interface { 23 | Users() UserService 24 | Relations() RelationService 25 | SMS() SMSService 26 | VCode() VCodeService 27 | } 28 | 29 | // service struct 30 | type service struct { 31 | repo repository.Repository 32 | } 33 | 34 | // New init service 35 | func New(repo repository.Repository) Service { 36 | return &service{ 37 | repo: repo, 38 | } 39 | } 40 | 41 | func (s *service) Users() UserService { 42 | return newUsers(s) 43 | } 44 | 45 | func (s *service) Relations() RelationService { 46 | return newRelations(s) 47 | } 48 | 49 | func (s *service) SMS() SMSService { 50 | return newSMS(s) 51 | } 52 | 53 | func (s *service) VCode() VCodeService { 54 | return newVCode(s) 55 | } 56 | -------------------------------------------------------------------------------- /internal/service/sms_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/qiniu/api.v7/auth" 6 | "github.com/qiniu/api.v7/sms" 7 | 8 | "github.com/go-eagle/eagle/internal/repository" 9 | ) 10 | 11 | // SMSService define interface func 12 | type SMSService interface { 13 | SendSMS(phoneNumber string, verifyCode int) error 14 | } 15 | 16 | type smsService struct { 17 | repo repository.Repository 18 | } 19 | 20 | var _ SMSService = (*smsService)(nil) 21 | 22 | func newSMS(svc *service) *smsService { 23 | return &smsService{repo: svc.repo} 24 | } 25 | 26 | // Send 发送短信 27 | func (s *smsService) SendSMS(phoneNumber string, verifyCode int) error { 28 | // 校验参数的正确性 29 | if phoneNumber == "" || verifyCode == 0 { 30 | return errors.New("param phone or verify_code error") 31 | } 32 | 33 | // 调用第三方发送服务 34 | return sendViaQiNiu(phoneNumber, verifyCode) 35 | } 36 | 37 | // sendViaQiNiu 调用七牛短信服务 38 | func sendViaQiNiu(phoneNumber string, verifyCode int) error { 39 | accessKey := "" 40 | secretKey := "" 41 | 42 | mac := auth.New(accessKey, secretKey) 43 | manager := sms.NewManager(mac) 44 | 45 | args := sms.MessagesRequest{ 46 | SignatureID: "", 47 | TemplateID: "", 48 | Mobiles: []string{phoneNumber}, 49 | Parameters: map[string]interface{}{ 50 | "code": verifyCode, 51 | }, 52 | } 53 | 54 | ret, err := manager.SendMessage(args) 55 | if err != nil { 56 | return errors.Wrap(err, "send sms message error") 57 | } 58 | 59 | if len(ret.JobID) == 0 { 60 | return errors.New("send sms message job id error") 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/service/sms_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_smsService_Send(t *testing.T) { 8 | type args struct { 9 | phoneNumber string 10 | verifyCode int 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantErr bool 16 | }{ 17 | // TODO: Add test cases. 18 | } 19 | 20 | s := New(nil) 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if err := s.SMS().SendSMS(tt.args.phoneNumber, tt.args.verifyCode); (err != nil) != tt.wantErr { 24 | t.Errorf("Send() error = %v, wantErr %v", err, tt.wantErr) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/service/trans.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/internal/model" 5 | ) 6 | 7 | // TransferUserInput 转换输入字段 8 | type TransferUserInput struct { 9 | CurUser *model.UserBaseModel 10 | User *model.UserBaseModel 11 | UserStat *model.UserStatModel 12 | IsFollow int `json:"is_follow"` 13 | IsFans int `json:"is_fans"` 14 | } 15 | 16 | // TransferUser 组装数据并输出 17 | // 对外暴露的user结构,都应该经过此结构进行转换 18 | func TransferUser(input *TransferUserInput) *model.UserInfo { 19 | if input.User == nil { 20 | return &model.UserInfo{} 21 | } 22 | 23 | return &model.UserInfo{ 24 | ID: input.User.ID, 25 | Username: input.User.Username, 26 | Avatar: input.User.Avatar, // todo: 转为url 27 | Sex: input.User.Sex, 28 | UserFollow: transferUserFollow(input), 29 | } 30 | } 31 | 32 | // transferUserFollow 转换用户关注相关字段 33 | func transferUserFollow(input *TransferUserInput) *model.UserFollow { 34 | followCount := 0 35 | if input.UserStat != nil { 36 | followCount = input.UserStat.FollowCount 37 | } 38 | followerCount := 0 39 | if input.UserStat != nil { 40 | followerCount = input.UserStat.FollowerCount 41 | } 42 | 43 | return &model.UserFollow{ 44 | FollowNum: followCount, 45 | FansNum: followerCount, 46 | IsFollow: input.IsFollow, 47 | IsFans: input.IsFans, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/templates/README.md: -------------------------------------------------------------------------------- 1 | # 前端页面模板 -------------------------------------------------------------------------------- /internal/templates/error/404.html: -------------------------------------------------------------------------------- 1 | {{ define "css"}} 2 | {{ end }} 3 | 4 | {{define "content"}} 5 |
6 |
7 |
8 |

9 | 404 页面未找到 10 |

11 |

12 | 页面你去哪了呀~~~ 13 |

14 |
15 |
16 |
17 | 18 | {{end}} 19 | 20 | {{ define "js"}} 21 | {{ end }} 22 | -------------------------------------------------------------------------------- /internal/templates/index.html: -------------------------------------------------------------------------------- 1 | {{ define "css"}} 2 | 5 | {{ end }} 6 | 7 | {{define "content"}} 8 | 9 | hello world 10 | 11 | {{end}} 12 | 13 | {{ define "js"}} 14 | {{ end }} -------------------------------------------------------------------------------- /internal/templates/layouts/master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ .title }} - eagle 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{/* */}} 12 | 13 | 14 | {{/* */}} 15 | {{/* */}} 16 | {{/* */}} 17 | {{block "css" .}} 18 | {{end}} 19 | 20 | 21 | {{include "layouts/header"}} 22 | 23 | {{template "content" .}} 24 | 25 | {{include "layouts/footer"}} 26 | 27 | {{block "js" .}} 28 | {{end}} 29 | 30 | -------------------------------------------------------------------------------- /internal/templates/partials/error.html: -------------------------------------------------------------------------------- 1 | {{if hasFlash .ctx}} 2 |
3 | {{flashMessage .ctx}} 4 |
5 | {{end}} -------------------------------------------------------------------------------- /internal/web/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 前端页面相关处理 -------------------------------------------------------------------------------- /internal/web/error.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Error404 return 404 page 10 | func Error404(c *gin.Context) { 11 | c.HTML(http.StatusOK, "error/404", gin.H{ 12 | "title": "404未找到", 13 | "ctx": c, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /internal/web/index.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Index home page 10 | func Index(c *gin.Context) { 11 | c.HTML(http.StatusOK, "index", gin.H{ 12 | "title": "首页", 13 | "ctx": c, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /internal/web/user/logout.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gorilla/sessions" 8 | 9 | "github.com/go-eagle/eagle/internal/web" 10 | "github.com/go-eagle/eagle/pkg/log" 11 | ) 12 | 13 | // Logout user logout 14 | func Logout(c *gin.Context) { 15 | // 删除cookie信息 16 | session := web.GetCookieSession(c) 17 | session.Options = &sessions.Options{ 18 | Domain: "", 19 | Path: "/", 20 | MaxAge: -1, 21 | } 22 | err := session.Save(web.Request(c), web.ResponseWriter(c)) 23 | if err != nil { 24 | log.Warnf("[user] logout save session err: %v", err) 25 | c.Abort() 26 | return 27 | } 28 | 29 | // 重定向得到原页面 30 | c.Redirect(http.StatusSeeOther, c.Request.Referer()) 31 | } 32 | -------------------------------------------------------------------------------- /internal/web/user/register.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/go-eagle/eagle/internal/model" 9 | "github.com/go-eagle/eagle/internal/service" 10 | "github.com/go-eagle/eagle/internal/web" 11 | "github.com/go-eagle/eagle/pkg/errcode" 12 | "github.com/go-eagle/eagle/pkg/flash" 13 | "github.com/go-eagle/eagle/pkg/log" 14 | ) 15 | 16 | // GetRegister register as a new user 17 | func GetRegister(c *gin.Context) { 18 | c.HTML(http.StatusOK, "user/register", gin.H{ 19 | "title": "注册", 20 | "ctx": c, 21 | }) 22 | } 23 | 24 | // DoRegister submit register 25 | func DoRegister(c *gin.Context) { 26 | log.Info("User Register function called.") 27 | var r RegisterRequest 28 | if err := c.Bind(&r); err != nil { 29 | web.Response(c, errcode.ErrInvalidParam, nil) 30 | return 31 | } 32 | 33 | u := model.UserBaseModel{ 34 | Username: r.Username, 35 | Email: r.Email, 36 | Password: r.Password, 37 | } 38 | 39 | // Validate the data. 40 | if err := u.Validate(); err != nil { 41 | web.Response(c, errcode.ErrValidation, nil) 42 | return 43 | } 44 | 45 | // Insert the user to the database. 46 | err := service.Svc.Users().Register(c, u.Username, u.Email, r.Password) 47 | if err != nil { 48 | web.Response(c, errcode.ErrInternalServer, nil) 49 | return 50 | } 51 | 52 | flash.SetMessage(c.Writer, "已发送激活链接,请检查您的邮箱。") 53 | 54 | // Show the user information. 55 | web.Response(c, nil, RegisterResponse{ 56 | ID: u.ID, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /internal/web/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | // CreateRequest create request 4 | type CreateRequest struct { 5 | Username string `json:"username"` 6 | Password string `json:"password"` 7 | Email string `json:"email"` 8 | } 9 | 10 | // LoginCredentials login request 11 | type LoginCredentials struct { 12 | Email string `json:"email" form:"email"` 13 | Password string `json:"password" form:"password"` 14 | } 15 | 16 | // RegisterRequest register request 17 | type RegisterRequest struct { 18 | Username string `json:"username" form:"username"` 19 | Email string `json:"email" form:"email"` 20 | Password string `json:"password" form:"password"` 21 | ConfirmPassword string `json:"confirm_password" form:"confirm_password"` 22 | } 23 | 24 | // RegisterResponse register response 25 | type RegisterResponse struct { 26 | ID uint64 `json:"id"` 27 | } 28 | 29 | // UpdateReq update request 30 | type UpdateReq struct { 31 | Status int `json:"status"` 32 | } 33 | 34 | // ListRequest list request 35 | type ListRequest struct { 36 | Username string `json:"username"` 37 | Offset int `json:"offset"` 38 | Limit int `json:"limit"` 39 | } 40 | -------------------------------------------------------------------------------- /pkg/app/config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var ( 8 | // Conf global app var 9 | Conf *Config 10 | ) 11 | 12 | // Config global config 13 | // nolint 14 | type Config struct { 15 | Name string 16 | Version string 17 | Mode string 18 | PprofPort string 19 | URL string 20 | JwtSecret string 21 | JwtTimeout int 22 | SSL bool 23 | CtxDefaultTimeout time.Duration 24 | CSRF bool 25 | Debug bool 26 | EnableTrace bool 27 | EnablePprof bool 28 | HTTP ServerConfig 29 | GRPC ServerConfig 30 | } 31 | 32 | // ServerConfig server config. 33 | type ServerConfig struct { 34 | Network string 35 | Addr string 36 | ReadTimeout time.Duration 37 | WriteTimeout time.Duration 38 | } 39 | -------------------------------------------------------------------------------- /pkg/app/doc.go: -------------------------------------------------------------------------------- 1 | // Package app eagle defines something for app 2 | package app 3 | -------------------------------------------------------------------------------- /pkg/app/form.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | // nolint: typecheck 8 | ut "github.com/go-playground/universal-translator" 9 | // nolint: typecheck 10 | val "github.com/go-playground/validator/v10" 11 | ) 12 | 13 | // ValidError . 14 | type ValidError struct { 15 | Key string 16 | Message string 17 | } 18 | 19 | // ValidErrors . 20 | type ValidErrors []*ValidError 21 | 22 | // Error return error msg 23 | func (v *ValidError) Error() string { 24 | return v.Message 25 | } 26 | 27 | // Error return error string 28 | func (v ValidErrors) Error() string { 29 | return strings.Join(v.Errors(), ",") 30 | } 31 | 32 | // Errors return some error 33 | func (v ValidErrors) Errors() []string { 34 | var errs []string 35 | for _, err := range v { 36 | errs = append(errs, err.Error()) 37 | } 38 | 39 | return errs 40 | } 41 | 42 | // BindAndValid valid params 43 | func BindAndValid(c *gin.Context, v interface{}) (bool, ValidErrors) { 44 | var errs ValidErrors 45 | err := c.ShouldBind(v) 46 | if err != nil { 47 | v := c.Value("trans") 48 | trans, _ := v.(ut.Translator) 49 | verrs, ok := err.(val.ValidationErrors) 50 | if !ok { 51 | return false, errs 52 | } 53 | 54 | for key, value := range verrs.Translate(trans) { 55 | errs = append(errs, &ValidError{ 56 | Key: key, 57 | Message: value, 58 | }) 59 | } 60 | 61 | return false, errs 62 | } 63 | 64 | return true, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/async/async.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-eagle/eagle/pkg/log" 7 | "github.com/go-eagle/eagle/pkg/utils" 8 | ) 9 | 10 | // Go 异步执行 asyncFunc() 函数,会进行 recover() 操作,如果出现 panic() 则会记录日志 11 | // name 用于 recover 后的日志和 metrics 统计 12 | func Go(ctx context.Context, name string, asyncFunc func()) { 13 | go func() { 14 | defer func() { 15 | if err := recover(); err != nil { 16 | stack := utils.StackTrace(name, err) 17 | log.WithContext(ctx).Errorf("async: name: %s panic: %s stack: %s", name, err, stack) 18 | // TODO: metrics 19 | } 20 | }() 21 | 22 | asyncFunc() 23 | }() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // HashAndSalt hash and salt the password 6 | func HashAndSalt(password string) (string, error) { 7 | hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 8 | if err != nil { 9 | return "", err 10 | } 11 | return string(hash), err 12 | } 13 | 14 | // ComparePasswords compare the hashed and plain passwords 15 | func ComparePasswords(hashed, plain string) bool { 16 | err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)) 17 | return err == nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/cache/README.md: -------------------------------------------------------------------------------- 1 | 这里主要是关于缓存的一些库,主要基于内存型和NoSQL的 2 | 3 | 内存型的有:memory和big_cache 4 | NoSQL的主要有: redis 5 | 6 | 各类库只要实现了cache定义的接口(driver)即可。 7 | > 这里的接口driver命名参考了Go官方mysql接口的命名规范 8 | 9 | ## 多级缓存 10 | 11 | ### 二级缓存 12 | 13 | 这里的多级主要是指二级缓存:本地缓存(一级缓存L1)+redis缓存(二级缓存L2) 14 | 使用本地缓存可以减少应用服务器到redis之间的网络I/O开销 15 | 16 | > 需要注意的是:在并发量不大的系统内,本地缓存的意义不大,反而增加维护的困难。但在高并发系统中, 17 | > 本地缓存可以大大节约带宽。但是要注意本地缓存不是银弹,它会引起多个副本间数据的 18 | > 不一致,还会占据大量的内存,所以不适合保存特别大的数据,而且需要严格考虑刷新机制。 19 | 20 | ### 过期时间 21 | 22 | 本地缓存过期时间比分布式缓存小至少一半,以防止本地缓存太久造成多实例数据不一致。 23 | 24 | ## 缓存问题 25 | 26 | 需要注意以下几个问题 27 | 28 | - 缓存穿透 29 | - 缓存击穿 30 | - 缓存雪崩 31 | 32 | 可以参考文章:[Redis缓存三大问题](https://mp.weixin.qq.com/s/HjzwefprYSGraU1aJcJ25g) 33 | 34 | ## Reference 35 | - ristretto:https://github.com/dgraph-io/ristretto (号称最快的本地缓存) 36 | - [Ristretto简介:高性能Go缓存](https://www.yuque.com/kshare/2020/ade1d9b5-5925-426a-9566-3a5587af2181) 37 | - bigcache: https://github.com/allegro/bigcache 38 | - freecache: https://github.com/coocood/freecache 39 | - concurrent_map: https://github.com/easierway/concurrent_map 40 | - gocache: https://github.com/eko/gocache (Built-in stores, eg: bigcache,memcache,redis) -------------------------------------------------------------------------------- /pkg/cache/getter.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // Getter . 4 | type Getter interface { 5 | Get(key string) interface{} 6 | } 7 | 8 | // GetFunc . 9 | type GetFunc func(key string) interface{} 10 | 11 | // Get . 12 | func (f GetFunc) Get(key string) interface{} { 13 | return f(key) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/cache/key.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // BuildCacheKey 构建一个带有前缀的缓存key 9 | func BuildCacheKey(keyPrefix string, key string) (cacheKey string, err error) { 10 | if key == "" { 11 | return "", errors.New("[cache] key should not be empty") 12 | } 13 | 14 | cacheKey = key 15 | if keyPrefix != "" { 16 | cacheKey, err = strings.Join([]string{keyPrefix, key}, ":"), nil 17 | } 18 | 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /pkg/cache/memory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/go-eagle/eagle/pkg/encoding" 10 | ) 11 | 12 | func Test_NewMemoryCache(t *testing.T) { 13 | asserts := assert.New(t) 14 | 15 | client := NewMemoryCache("prefix", encoding.JSONEncoding{}) 16 | asserts.NotNil(client) 17 | } 18 | 19 | func TestMemoStore_Set(t *testing.T) { 20 | asserts := assert.New(t) 21 | 22 | store := NewMemoryCache("prefix", encoding.JSONEncoding{}) 23 | err := store.Set(context.Background(), "test-key", "test-val", -1) 24 | asserts.NotNil(err) 25 | } 26 | 27 | func TestMemoStore_Get(t *testing.T) { 28 | asserts := assert.New(t) 29 | store := NewMemoryCache("prefix", encoding.JSONEncoding{}) 30 | ctx := context.Background() 31 | 32 | type testStruct struct { 33 | Name string 34 | Age int 35 | } 36 | 37 | // normal 38 | { 39 | testKey := "test-key2" 40 | testVal := testStruct{ 41 | Name: "test-name", 42 | Age: 18, 43 | } 44 | err := store.Set(ctx, testKey, &testVal, 60) 45 | asserts.Nil(err) 46 | 47 | var gotVal testStruct 48 | err = store.Get(ctx, testKey, &gotVal) 49 | asserts.Nil(err) 50 | asserts.NotNil(gotVal) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/cache/metric.go: -------------------------------------------------------------------------------- 1 | package cache 2 | -------------------------------------------------------------------------------- /pkg/client/consulclient/client.go: -------------------------------------------------------------------------------- 1 | package consulclient 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/hashicorp/consul/api" 9 | 10 | "github.com/go-eagle/eagle/pkg/config" 11 | ) 12 | 13 | // Config consul config 14 | type Config struct { 15 | Addr string 16 | Scheme string 17 | Datacenter string 18 | WaitTime time.Duration 19 | Namespace string 20 | } 21 | 22 | func New() (*api.Client, error) { 23 | cfg, err := loadConf() 24 | if err != nil { 25 | panic(fmt.Sprintf("[etcd] load consul conf err: %v", err)) 26 | } 27 | return newClient(cfg) 28 | } 29 | 30 | func newClient(cfg *Config) (*api.Client, error) { 31 | consulClient, err := api.NewClient(&api.Config{ 32 | Address: cfg.Addr, 33 | Scheme: cfg.Scheme, 34 | Datacenter: cfg.Datacenter, 35 | WaitTime: cfg.WaitTime, 36 | Namespace: cfg.Namespace, 37 | }) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | return consulClient, nil 42 | } 43 | 44 | // loadConf load register config 45 | func loadConf() (ret *Config, err error) { 46 | var cfg Config 47 | v, err := config.LoadWithType("registry", config.FileTypeYaml) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | err = v.UnmarshalKey("consul", &cfg) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return &cfg, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/client/etcdclient/client_test.go: -------------------------------------------------------------------------------- 1 | package etcdclient 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "go.etcd.io/etcd/client/v3/mock/mockserver" 9 | ) 10 | 11 | func startMockServer() { 12 | ms, err := mockserver.StartMockServers(1) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | if err := ms.StartAt(0); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | 22 | func TestMain(m *testing.M) { 23 | go startMockServer() 24 | } 25 | 26 | func Test_newClient(t *testing.T) { 27 | config := &Config{} 28 | config.Endpoints = []string{"localhost:0"} 29 | config.TTL = 5 30 | _, err := newClient(config) 31 | assert.Nil(t, err) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/client/httpclient/options.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import "time" 4 | 5 | // Option is a function that sets some option on the client. 6 | type Option func(c *options) 7 | 8 | // Options control behavior of the client. 9 | type options struct { 10 | header map[string][]string 11 | // timeout of per request 12 | timeout time.Duration 13 | } 14 | 15 | func defaultOptions() *options { 16 | return &options{ 17 | header: make(map[string][]string), 18 | timeout: DefaultTimeout, 19 | } 20 | } 21 | 22 | // WithTimeout with a timeout for per request 23 | func WithTimeout(duration time.Duration) Option { 24 | return func(cfg *options) { 25 | cfg.timeout = duration 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/client/httpclient/otelhttp.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "go.opentelemetry.io/contrib" 5 | "go.opentelemetry.io/otel" 6 | "go.opentelemetry.io/otel/trace" 7 | ) 8 | 9 | const ( 10 | traceName = "github.com/go-eagle/eagle/pkg/net/http" 11 | ) 12 | 13 | var ( 14 | tracer trace.Tracer 15 | ) 16 | 17 | // nolint 18 | func init() { 19 | tracer = otel.GetTracerProvider().Tracer(traceName, trace.WithInstrumentationVersion(contrib.SemVersion())) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/config/README.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | - env 4 | - file 5 | - etcd -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLoad(t *testing.T) { 11 | // AppConfig app config 12 | type AppConfig struct { 13 | Name string 14 | Version string 15 | Mode string 16 | PprofPort string 17 | URL string 18 | JwtSecret string 19 | JwtTimeout int 20 | SSL bool 21 | CtxDefaultTimeout time.Duration 22 | CSRF bool 23 | Debug bool 24 | } 25 | var config AppConfig 26 | 27 | t.Run("using yaml config", func(t *testing.T) { 28 | 29 | c := New("./testdata") 30 | err := c.Load("app", &config) 31 | assert.Nil(t, err) 32 | assert.NotNil(t, config) 33 | assert.Equal(t, "eagle", config.Name) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Option config option 4 | type Option func(*Config) 5 | 6 | // WithFileType config file type 7 | func WithFileType(fileType string) Option { 8 | return func(c *Config) { 9 | c.configType = fileType 10 | } 11 | } 12 | 13 | // WithEnv env var 14 | func WithEnv(name string) Option { 15 | return func(c *Config) { 16 | c.env = name 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/config/testdata/app.yaml: -------------------------------------------------------------------------------- 1 | Name: eagle 2 | Version: 1.0.0 3 | PprofPort: :5555 4 | Mode: debug # debug, release, test 5 | JwtSecret: JWT_SECRET 6 | JwtTimeout: 86400 7 | CookieName: jwt-token 8 | SSL: true 9 | CtxDefaultTimeout: 12 10 | CSRF: true 11 | Debug: false -------------------------------------------------------------------------------- /pkg/container/group/example_test.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import "fmt" 4 | 5 | type Counter struct { 6 | Value int 7 | } 8 | 9 | func (c *Counter) Incr() { 10 | c.Value++ 11 | } 12 | 13 | func ExampleGroup_Get() { 14 | new := func() interface{} { 15 | fmt.Println("Only Once") 16 | return &Counter{} 17 | } 18 | group := NewGroup(new) 19 | 20 | // Create a new Counter 21 | group.Get("pass").(*Counter).Incr() 22 | 23 | // Get the created Counter again. 24 | group.Get("pass").(*Counter).Incr() 25 | // Output: 26 | // Only Once 27 | } 28 | 29 | func ExampleGroup_Reset() { 30 | new := func() interface{} { 31 | return &Counter{} 32 | } 33 | group := NewGroup(new) 34 | 35 | newV2 := func() interface{} { 36 | fmt.Println("New V2") 37 | return &Counter{} 38 | } 39 | // Reset the new function and clear all created objects. 40 | group.Reset(newV2) 41 | 42 | // Create a new Counter 43 | group.Get("pass").(*Counter).Incr() 44 | // Output: 45 | // New V2 46 | } 47 | -------------------------------------------------------------------------------- /pkg/container/group/group.go: -------------------------------------------------------------------------------- 1 | // Package group provides a sample lazy load container. 2 | // The group only creating a new object not until the object is needed by user. 3 | // And it will cache all the objects to reduce the creation of object. 4 | package group 5 | 6 | import "sync" 7 | 8 | // Group is a lazy load container. 9 | type Group struct { 10 | new func() interface{} 11 | objs map[string]interface{} 12 | sync.RWMutex 13 | } 14 | 15 | // NewGroup news a group container. 16 | func NewGroup(new func() interface{}) *Group { 17 | if new == nil { 18 | panic("container.group: can't assign a nil to the new function") 19 | } 20 | return &Group{ 21 | new: new, 22 | objs: make(map[string]interface{}), 23 | } 24 | } 25 | 26 | // Get gets the object by the given key. 27 | func (g *Group) Get(key string) interface{} { 28 | g.RLock() 29 | obj, ok := g.objs[key] 30 | if ok { 31 | g.RUnlock() 32 | return obj 33 | } 34 | g.RUnlock() 35 | 36 | // double check 37 | g.Lock() 38 | defer g.Unlock() 39 | obj, ok = g.objs[key] 40 | if ok { 41 | return obj 42 | } 43 | 44 | // new a obj if not exist 45 | obj = g.new() 46 | g.objs[key] = obj 47 | return obj 48 | } 49 | 50 | // Reset resets the new function and deletes all existing objects. 51 | func (g *Group) Reset(new func() interface{}) { 52 | if new == nil { 53 | panic("container.group: can't assign a nil to the new function") 54 | } 55 | g.Lock() 56 | g.new = new 57 | g.Unlock() 58 | g.Clear() 59 | } 60 | 61 | // Clear deletes all objects. 62 | func (g *Group) Clear() { 63 | g.Lock() 64 | g.objs = make(map[string]interface{}) 65 | g.Unlock() 66 | } 67 | -------------------------------------------------------------------------------- /pkg/container/group/group_test.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGroupGet(t *testing.T) { 10 | count := 0 11 | g := NewGroup(func() interface{} { 12 | count++ 13 | return count 14 | }) 15 | v := g.Get("/v1/users/1") 16 | assert.Equal(t, 1, v.(int)) 17 | 18 | v = g.Get("/v1/users/1/avatar") 19 | assert.Equal(t, 2, v.(int)) 20 | 21 | v = g.Get("/v1/users/1") 22 | assert.Equal(t, 1, v.(int)) 23 | assert.Equal(t, 2, count) 24 | } 25 | 26 | func TestGroupReset(t *testing.T) { 27 | g := NewGroup(func() interface{} { 28 | return 1 29 | }) 30 | g.Get("/v1/users/1") 31 | call := false 32 | g.Reset(func() interface{} { 33 | call = true 34 | return 1 35 | }) 36 | 37 | length := 0 38 | for range g.objs { 39 | length++ 40 | } 41 | 42 | assert.Equal(t, 0, length) 43 | 44 | g.Get("/v1/users/1") 45 | assert.Equal(t, true, call) 46 | } 47 | 48 | func TestGroupClear(t *testing.T) { 49 | g := NewGroup(func() interface{} { 50 | return 1 51 | }) 52 | g.Get("/v1/users/1") 53 | length := 0 54 | for range g.objs { 55 | length++ 56 | } 57 | assert.Equal(t, 1, length) 58 | 59 | g.Clear() 60 | length = 0 61 | for range g.objs { 62 | length++ 63 | } 64 | assert.Equal(t, 0, length) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/email/init.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/go-eagle/eagle/pkg/log" 8 | ) 9 | 10 | // Client 邮件发送客户端 11 | var Client Driver 12 | 13 | // Lock 读写锁 14 | var Lock sync.RWMutex 15 | 16 | var ( 17 | // ErrChanNotOpen 邮件队列没有开启 18 | ErrChanNotOpen = errors.New("email queue does not open") 19 | ) 20 | 21 | // Config email config 22 | type Config struct { 23 | Host string 24 | Port int 25 | Username string 26 | Password string 27 | Name string 28 | Address string 29 | ReplyTo string 30 | KeepAlive int 31 | } 32 | 33 | // Init 初始化客户端 34 | func Init(cfg Config) { 35 | log.Info("email init") 36 | Lock.Lock() 37 | defer Lock.Unlock() 38 | 39 | // 确保是已经关闭的 40 | if Client != nil { 41 | Client.Close() 42 | } 43 | 44 | client := NewSMTPClient(SMTPConfig{ 45 | Name: cfg.Name, 46 | Address: cfg.Address, 47 | ReplyTo: cfg.ReplyTo, 48 | Host: cfg.Host, 49 | Port: cfg.Port, 50 | Username: cfg.Username, 51 | Password: cfg.Password, 52 | Keepalive: cfg.KeepAlive, 53 | }) 54 | 55 | Client = client 56 | } 57 | -------------------------------------------------------------------------------- /pkg/email/mail.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | // Driver 邮件发送驱动接口定义 4 | type Driver interface { 5 | // Send 发送邮件 6 | Send(to, subject, body string) error 7 | // Close 关闭链接 8 | Close() 9 | } 10 | 11 | // Send 发送邮件 12 | func Send(to, subject, body string) error { 13 | Lock.RLock() 14 | defer Lock.RUnlock() 15 | 16 | if Client == nil { 17 | return nil 18 | } 19 | 20 | return Client.Send(to, subject, body) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/email/templates/active-mail.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-eagle/eagle/1b5c80f19311299eb51c4f8c4f1dba7adf0e2210/pkg/email/templates/active-mail.html -------------------------------------------------------------------------------- /pkg/encoding/encoding_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "testing" 4 | 5 | func BenchmarkJsonMarshal(b *testing.B) { 6 | a := make([]int, 0, 400) 7 | for i := 0; i < 400; i++ { 8 | a = append(a, i) 9 | } 10 | jsonEncoding := JSONEncoding{} 11 | for n := 0; n < b.N; n++ { 12 | _, err := jsonEncoding.Marshal(a) 13 | if err != nil { 14 | b.Error(err) 15 | } 16 | } 17 | } 18 | 19 | func BenchmarkJsonUnmarshal(b *testing.B) { 20 | a := make([]int, 0, 400) 21 | for i := 0; i < 400; i++ { 22 | a = append(a, i) 23 | } 24 | jsonEncoding := JSONEncoding{} 25 | data, err := jsonEncoding.Marshal(a) 26 | if err != nil { 27 | b.Error(err) 28 | } 29 | var result []int 30 | for n := 0; n < b.N; n++ { 31 | err = jsonEncoding.Unmarshal(data, &result) 32 | if err != nil { 33 | b.Error(err) 34 | } 35 | } 36 | } 37 | 38 | func BenchmarkMsgpack(b *testing.B) { 39 | // run the Fib function b.N times 40 | a := make([]int, 400) 41 | for i := 0; i < 400; i++ { 42 | a = append(a, i) 43 | } 44 | msgPackEncoding := MsgPackEncoding{} 45 | data, err := msgPackEncoding.Marshal(a) 46 | if err != nil { 47 | b.Error(err) 48 | } 49 | var result []int 50 | for n := 0; n < b.N; n++ { 51 | err = msgPackEncoding.Unmarshal(data, &result) 52 | if err != nil { 53 | b.Error(err) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/encoding/example_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | -------------------------------------------------------------------------------- /pkg/encoding/gob_encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | ) 7 | 8 | // GobEncoding gob encode 9 | type GobEncoding struct{} 10 | 11 | // Marshal gob encode 12 | func (g GobEncoding) Marshal(v interface{}) ([]byte, error) { 13 | var ( 14 | buffer bytes.Buffer 15 | ) 16 | 17 | err := gob.NewEncoder(&buffer).Encode(v) 18 | return buffer.Bytes(), err 19 | } 20 | 21 | // Unmarshal gob encode 22 | func (g GobEncoding) Unmarshal(data []byte, value interface{}) error { 23 | err := gob.NewDecoder(bytes.NewReader(data)).Decode(value) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/encoding/msgpack_encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "github.com/vmihailenco/msgpack" 4 | 5 | // MsgPackEncoding msgpack 格式 6 | type MsgPackEncoding struct{} 7 | 8 | // Marshal msgpack encode 9 | func (mp MsgPackEncoding) Marshal(v interface{}) ([]byte, error) { 10 | buf, err := msgpack.Marshal(v) 11 | return buf, err 12 | } 13 | 14 | // Unmarshal msgpack decode 15 | func (mp MsgPackEncoding) Unmarshal(data []byte, value interface{}) error { 16 | err := msgpack.Unmarshal(data, value) 17 | if err != nil { 18 | return err 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /pkg/encoding/proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/golang/protobuf/proto" 7 | 8 | "github.com/go-eagle/eagle/pkg/encoding" 9 | ) 10 | 11 | // Name is the name registered for the proto compressor. 12 | const Name = "proto" 13 | 14 | func init() { 15 | encoding.RegisterCodec(codec{}) 16 | } 17 | 18 | // codec is a Codec implementation with protobuf. It is the default codec for gRPC. 19 | type codec struct{} 20 | 21 | func (codec) Marshal(v interface{}) ([]byte, error) { 22 | vv, ok := v.(proto.Message) 23 | if !ok { 24 | return nil, fmt.Errorf("failed to marshal, message is %T, want proto.Message", v) 25 | } 26 | return proto.Marshal(vv) 27 | } 28 | 29 | func (codec) Unmarshal(data []byte, v interface{}) error { 30 | vv, ok := v.(proto.Message) 31 | if !ok { 32 | return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v) 33 | } 34 | return proto.Unmarshal(data, vv) 35 | } 36 | 37 | func (codec) Name() string { 38 | return Name 39 | } 40 | -------------------------------------------------------------------------------- /pkg/errcode/README.md: -------------------------------------------------------------------------------- 1 | ## 错误码设计 2 | 3 | > 参考 新浪开放平台 [Error code](http://open.weibo.com/wiki/Error_code) 的设计 4 | 5 | #### 错误返回值格式 6 | 7 | ```json 8 | { 9 | "code": 10002, 10 | "message": "Error occurred while binding the request body to the struct." 11 | } 12 | ``` 13 | 14 | #### 错误代码说明 15 | 16 | | 1 | 00 | 02 | 17 | | :------ | :------ | :------ | 18 | | 服务级错误(1为系统级错误) | 服务模块代码 | 具体错误代码 | 19 | 20 | - 服务级别错误:1 为系统级错误;2 为普通错误,通常是由用户非法操作引起的 21 | - 服务模块为两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了 22 | - 错误码为两位数:防止一个模块定制过多的错误码,后期不好维护 23 | - `code = 0` 说明是正确返回,`code > 0` 说明是错误返回 24 | - 错误通常包括系统级错误码和服务级错误码 25 | - 建议代码中按服务模块将错误分类 26 | - 错误码均为 >= 0 的数 27 | - 在本项目中 HTTP Code 固定为 http.StatusOK,错误码通过 code 来表示。 28 | 29 | 30 | ## Reference 31 | 32 | - [gRPC错误处理](https://mp.weixin.qq.com/s/ghJiTvJxYzLKTFs5gZga5w) 33 | 34 | -------------------------------------------------------------------------------- /pkg/errcode/code.go: -------------------------------------------------------------------------------- 1 | package errcode 2 | 3 | //nolint: golint 4 | var ( 5 | // 预定义错误 6 | // Common errors 7 | Success = NewError(0, "Ok") 8 | ErrInternalServer = NewError(10000, "Internal server error") 9 | ErrInvalidParam = NewError(10001, "Invalid params") 10 | ErrUnauthorized = NewError(10002, "Unauthorized error") 11 | ErrNotFound = NewError(10003, "Not found") 12 | ErrUnknown = NewError(10004, "Unknown") 13 | ErrDeadlineExceeded = NewError(10005, "Deadline exceeded") 14 | ErrAccessDenied = NewError(10006, "Access denied") 15 | ErrLimitExceed = NewError(10007, "Beyond limit") 16 | ErrMethodNotAllowed = NewError(10008, "Method not allowed") 17 | ErrSignParam = NewError(10011, "Invalid sign") 18 | ErrValidation = NewError(10012, "Validation failed") 19 | ErrDatabase = NewError(10013, "Database error") 20 | ErrToken = NewError(10014, "Gen token error") 21 | ErrInvalidToken = NewError(10015, "Invalid token") 22 | ErrTokenTimeout = NewError(10016, "Token timeout") 23 | ErrTooManyRequests = NewError(10017, "Too many request") 24 | ErrInvalidTransaction = NewError(10018, "Invalid transaction") 25 | ErrEncrypt = NewError(10019, "Encrypting the user password error") 26 | ErrServiceUnavailable = NewError(10020, "Service Unavailable") 27 | ) 28 | -------------------------------------------------------------------------------- /pkg/flash/session.go: -------------------------------------------------------------------------------- 1 | package flash 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/sessions" 8 | 9 | "github.com/go-eagle/eagle/pkg/log" 10 | ) 11 | 12 | // Store cookie storage 13 | var Store = sessions.NewCookieStore([]byte("secret-password")) 14 | 15 | //var sessionName = "flash-session" 16 | 17 | //GetCurrentUserName returns the username of the logged in user 18 | func GetCurrentUserName(r *http.Request) string { 19 | session, err := Store.Get(r, "session") 20 | if err == nil { 21 | return session.Values["username"].(string) 22 | } 23 | return "" 24 | } 25 | 26 | // SetFlashMessage set flash msg 27 | func SetFlashMessage(w http.ResponseWriter, r *http.Request, name string, value string) { 28 | session, err := Store.Get(r, flashName) 29 | if err != nil { 30 | log.Warnf("[session] set flash message err: %v", err) 31 | } 32 | session.AddFlash(value, name) 33 | err = session.Save(r, w) 34 | if err != nil { 35 | log.Warnf("[session] session save err: %v", err) 36 | } 37 | } 38 | 39 | // GetFlashMessage get flash msg from session 40 | func GetFlashMessage(w http.ResponseWriter, r *http.Request, name string) string { 41 | session, err := Store.Get(r, flashName) 42 | if err != nil { 43 | log.Warnf("[session] get flash message err: %v", err) 44 | return "" 45 | } 46 | 47 | fm := session.Flashes(name) 48 | if fm == nil { 49 | fmt.Fprint(w, "No flash messages") 50 | return "" 51 | } 52 | _ = session.Save(r, w) 53 | _, _ = fmt.Fprintf(w, "%v", fm[0]) 54 | 55 | return fm[0].(string) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/lock/lock.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | const ( 8 | // RedisLockKey redis lock key 9 | RedisLockKey = "eagle:redis:lock:%s" 10 | // EtcdLockKey etcd lock key 11 | EtcdLockKey = "/eagle/lock/%s" 12 | ) 13 | 14 | // Lock define common func 15 | type Lock interface { 16 | Lock(ctx context.Context) (bool, error) 17 | Unlock(ctx context.Context) (bool, error) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/lock/luascript.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | var ( 4 | // lockscript lua script for acrequire a lock 5 | lockLuaScript = ` 6 | if redis.call("GET", KEYS[1]) == ARGV[1] then 7 | redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2]) 8 | return "OK" 9 | else 10 | return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) 11 | end 12 | ` 13 | 14 | // unlockscript lua script for release a lock 15 | unlockLuaScript = ` 16 | if redis.call("GET", KEYS[1]) == ARGV[1] then 17 | return redis.call("DEL", KEYS[1]) 18 | else 19 | return 0 20 | end 21 | ` 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/lock/utils.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | // genToken 生成token 8 | func genToken() string { 9 | u, _ := uuid.NewRandom() 10 | return u.String() 11 | } 12 | -------------------------------------------------------------------------------- /pkg/log/README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 3 | 本logger主要是对zap库的封装,便于使用。当然也可以根据接口使用其他日志库,比如 `logrus`。 4 | 5 | ## 日志功能 6 | 7 | - 将日志信息记录到日志文件里 8 | - 日志切割-能够根据日志文件大小或时间间隔进行切割 9 | - 支持不同的日志级别(eg:info,debug,warn,error,fatal) 10 | - 支持按日志级别分类输出到不同日志文件 11 | - 能够打印基本信息,如调用文件/函数名和行号,日志时间,IP等 12 | 13 | ## 使用方法 14 | 15 | ```go 16 | log.Info("user_id is 1") 17 | log.Warn("user is not exist") 18 | log.Error("params error") 19 | 20 | log.Warnf("params is empty") 21 | ... 22 | ``` 23 | 24 | ## 原则 25 | 26 | 日志尽量不要在 model, repository, service中打印输出,最好使用 `errors.Wrapf` 将错误和消息返回到上层,然后在 handler 层中处理错误, 27 | 也就是通过日志打印出来。 28 | 29 | 这样做的好处是:避免相同日志在多个地方打印,让排查问题更简单。 30 | 31 | ## Reference 32 | 33 | - 日志基础库 zap: https://github.com/uber-go/zap 34 | - 日志分割库-按时间:https://github.com/lestrrat-go/file-rotatelogs 35 | - 日志分割库-按大小:https://github.com/natefinch/lumberjack 36 | - [深度 | 从Go高性能日志库zap看如何实现高性能Go组件](https://mp.weixin.qq.com/s/i0bMh_gLLrdnhAEWlF-xDw) 37 | - [Logger interface for GO with zap and logrus implementation](https://www.mountedthoughts.com/golang-logger-interface/) 38 | - https://github.com/wzyonggege/logger 39 | - https://wisp888.github.io/golang-iris-%E5%AD%A6%E4%B9%A0%E4%BA%94-zap%E6%97%A5%E5%BF%97.html 40 | - https://www.mountedthoughts.com/golang-logger-interface/ 41 | -------------------------------------------------------------------------------- /pkg/log/config.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "time" 4 | 5 | // Config log config 6 | type Config struct { 7 | Development bool 8 | DisableCaller bool 9 | DisableStacktrace bool 10 | Encoding string 11 | Level string 12 | ServiceName string // service name 13 | Filename string 14 | Writers string 15 | LoggerDir string 16 | LogFormatText bool 17 | LogRollingPolicy string 18 | LogBackupCount uint 19 | FlushInterval time.Duration // default is 30s, recommend is dev or test is 1s, prod is 1m 20 | } 21 | -------------------------------------------------------------------------------- /pkg/log/logger_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewLogger(t *testing.T) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pkg/log/options.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | type Option func(*Config) 4 | 5 | // WithFilename set log filename 6 | func WithFilename(filename string) Option { 7 | return func(cfg *Config) { 8 | cfg.Filename = filename 9 | } 10 | } 11 | 12 | // WithLogDir set log dir 13 | func WithLogDir(dir string) Option { 14 | return func(cfg *Config) { 15 | cfg.LoggerDir = dir 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/log/utils.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/go-eagle/eagle/pkg/utils" 4 | 5 | // GetLogFile get log file absolute path 6 | func GetLogFile(filename string, suffix string) string { 7 | return utils.ConcatString(logDir, "/", hostname, "/", filename, suffix) 8 | } 9 | -------------------------------------------------------------------------------- /pkg/metric/counter.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | var _ CounterVec = (*promCounterVec)(nil) 8 | 9 | // CounterVecOpts is an alias of VectorOpts. 10 | type CounterVecOpts VectorOpts 11 | 12 | // counterVec counter vec. 13 | type promCounterVec struct { 14 | counter *prometheus.CounterVec 15 | } 16 | 17 | // NewCounterVec . 18 | func NewCounterVec(cfg *CounterVecOpts) CounterVec { 19 | if cfg == nil { 20 | return nil 21 | } 22 | vec := prometheus.NewCounterVec( 23 | prometheus.CounterOpts{ 24 | Namespace: cfg.Namespace, 25 | Subsystem: cfg.Subsystem, 26 | Name: cfg.Name, 27 | Help: cfg.Help, 28 | }, cfg.Labels) 29 | prometheus.MustRegister(vec) 30 | return &promCounterVec{ 31 | counter: vec, 32 | } 33 | } 34 | 35 | // Inc Inc increments the counter by 1. Use Add to increment it by arbitrary. 36 | func (counter *promCounterVec) Inc(labels ...string) { 37 | counter.counter.WithLabelValues(labels...).Inc() 38 | } 39 | 40 | // Add Inc increments the counter by 1. Use Add to increment it by arbitrary. 41 | func (counter *promCounterVec) Add(v float64, labels ...string) { 42 | counter.counter.WithLabelValues(labels...).Add(v) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/metric/counter_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus/testutil" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewCounterVec(t *testing.T) { 11 | counterVec := NewCounterVec(&CounterVecOpts{ 12 | Namespace: "http_server", 13 | Subsystem: "requests", 14 | Name: "total", 15 | Help: "rpc client requests error count.", 16 | }) 17 | counterVecNil := NewCounterVec(nil) 18 | assert.NotNil(t, counterVec) 19 | assert.Nil(t, counterVecNil) 20 | } 21 | 22 | func TestCounterIncr(t *testing.T) { 23 | counterVec := NewCounterVec(&CounterVecOpts{ 24 | Namespace: "http_client", 25 | Subsystem: "call", 26 | Name: "code_total", 27 | Help: "http client requests error count.", 28 | Labels: []string{"path", "code"}, 29 | }) 30 | cv, _ := counterVec.(*promCounterVec) 31 | cv.Inc("/v1/users", "500") 32 | cv.Inc("/v1/users", "500") 33 | r := testutil.ToFloat64(cv.counter) 34 | assert.Equal(t, float64(2), r) 35 | } 36 | 37 | func TestCounterAdd(t *testing.T) { 38 | counterVec := NewCounterVec(&CounterVecOpts{ 39 | Namespace: "rpc_server", 40 | Subsystem: "requests", 41 | Name: "err_total", 42 | Help: "rpc client requests error count.", 43 | Labels: []string{"method", "code"}, 44 | }) 45 | cv, _ := counterVec.(*promCounterVec) 46 | cv.Add(11, "/v1/users", "500") 47 | cv.Add(22, "/v1/users", "500") 48 | r := testutil.ToFloat64(cv.counter) 49 | assert.Equal(t, float64(33), r) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/metric/gauge.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | var _ GaugeVec = (*promGaugeVec)(nil) 8 | 9 | // GaugeVecOpts is an alias of VectorOpts. 10 | type GaugeVecOpts VectorOpts 11 | 12 | // gaugeVec gauge vec. 13 | type promGaugeVec struct { 14 | gauge *prometheus.GaugeVec 15 | } 16 | 17 | // NewGaugeVec . 18 | func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec { 19 | if cfg == nil { 20 | return nil 21 | } 22 | vec := prometheus.NewGaugeVec( 23 | prometheus.GaugeOpts{ 24 | Namespace: cfg.Namespace, 25 | Subsystem: cfg.Subsystem, 26 | Name: cfg.Name, 27 | Help: cfg.Help, 28 | }, cfg.Labels) 29 | prometheus.MustRegister(vec) 30 | return &promGaugeVec{ 31 | gauge: vec, 32 | } 33 | } 34 | 35 | // Inc increments the counter by 1. Use Inc to increment it by arbitrary. 36 | func (gauge *promGaugeVec) Inc(labels ...string) { 37 | gauge.gauge.WithLabelValues(labels...).Inc() 38 | } 39 | 40 | // Dec increments the counter by 1. Use Dec to increment it by arbitrary. 41 | func (gauge *promGaugeVec) Dec(labels ...string) { 42 | gauge.gauge.WithLabelValues(labels...).Dec() 43 | } 44 | 45 | // Add Inc increments the counter by 1. Use Add to increment it by arbitrary. 46 | func (gauge *promGaugeVec) Add(v float64, labels ...string) { 47 | gauge.gauge.WithLabelValues(labels...).Add(v) 48 | } 49 | 50 | // Set set the given value to the collection. 51 | func (gauge *promGaugeVec) Set(v float64, labels ...string) { 52 | gauge.gauge.WithLabelValues(labels...).Set(v) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/metric/histogram.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | var _ HistogramVec = (*promHistogramVec)(nil) 8 | 9 | // HistogramVecOpts is histogram vector opts. 10 | type HistogramVecOpts struct { 11 | Namespace string 12 | Subsystem string 13 | Name string 14 | Help string 15 | Labels []string 16 | Buckets []float64 17 | } 18 | 19 | // Histogram prom histogram collection. 20 | type promHistogramVec struct { 21 | histogram *prometheus.HistogramVec 22 | } 23 | 24 | // NewHistogramVec new a histogram vec. 25 | func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec { 26 | if cfg == nil { 27 | return nil 28 | } 29 | vec := prometheus.NewHistogramVec( 30 | prometheus.HistogramOpts{ 31 | Namespace: cfg.Namespace, 32 | Subsystem: cfg.Subsystem, 33 | Name: cfg.Name, 34 | Help: cfg.Help, 35 | Buckets: cfg.Buckets, 36 | }, cfg.Labels) 37 | prometheus.MustRegister(vec) 38 | return &promHistogramVec{ 39 | histogram: vec, 40 | } 41 | } 42 | 43 | // Timing adds a single observation to the histogram. 44 | func (histogram *promHistogramVec) Observe(v int64, labels ...string) { 45 | histogram.histogram.WithLabelValues(labels...).Observe(float64(v)) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/metric/histogram_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/prometheus/client_golang/prometheus/testutil" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewHistogramVec(t *testing.T) { 12 | histogramVec := NewHistogramVec(&HistogramVecOpts{ 13 | Name: "duration_ms", 14 | Help: "rpc server requests duration(ms).", 15 | Buckets: []float64{1, 2, 3}, 16 | }) 17 | histogramVecNil := NewHistogramVec(nil) 18 | assert.NotNil(t, histogramVec) 19 | assert.Nil(t, histogramVecNil) 20 | } 21 | 22 | func TestHistogramObserve(t *testing.T) { 23 | histogramVec := NewHistogramVec(&HistogramVecOpts{ 24 | Name: "counts", 25 | Help: "rpc server requests duration(ms).", 26 | Buckets: []float64{1, 2, 3}, 27 | Labels: []string{"method"}, 28 | }) 29 | hv, _ := histogramVec.(*promHistogramVec) 30 | hv.Observe(2, "/v1/users") 31 | 32 | metadata := ` 33 | # HELP counts rpc server requests duration(ms). 34 | # TYPE counts histogram 35 | ` 36 | val := ` 37 | counts_bucket{method="/v1/users",le="1"} 0 38 | counts_bucket{method="/v1/users",le="2"} 1 39 | counts_bucket{method="/v1/users",le="3"} 1 40 | counts_bucket{method="/v1/users",le="+Inf"} 1 41 | counts_sum{method="/v1/users"} 2 42 | counts_count{method="/v1/users"} 1 43 | ` 44 | 45 | err := testutil.CollectAndCompare(hv.histogram, strings.NewReader(metadata+val)) 46 | assert.Nil(t, err) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/middleware/README.md: -------------------------------------------------------------------------------- 1 | Gin 自带的中间件有很多种,可以在 [https://github.com/gin-gonic/contrib](https://github.com/gin-gonic/contrib) 找到。 2 | 3 | 下面是一些常用的中间件 4 | 5 | - RestGate:REST API 端点的安全身份验证 6 | - gin-jwt:用于 Gin 框架的 JWT 中间件 7 | - gin-sessions:基于 MongoDB 和 MySQL 的会话中间件 8 | - gin-location:用于公开服务器主机名和方案的中间件 9 | - gin-nice-recovery:异常错误恢复中间件,让您构建更好的用户体验 10 | - gin-limit:限制同时请求,可以帮助解决高流量负载 11 | - gin-oauth2:用于处理 OAuth2 12 | - gin-template:简单易用的 Gin 框架 HTML/模板 13 | - gin-redis-ip-limiter:基于 IP 地址的请求限制器 14 | - gin-access-limit:通过指定允许的源 CIDR 表示法来访问控制中间件 15 | - gin-session:Gin 的会话中间件 16 | - gin-stats:轻量级且有用的请求指标中间件 17 | - gin-session-middleware:一个高效,安全且易于使用的 Go 会话库 18 | - ginception:漂亮的异常页面 19 | - gin-inspector:用于调查 HTTP 请求的 Gin 中间件 20 | 21 | ## Reference 22 | - https://github.com/chenjiandongx/ginprom -------------------------------------------------------------------------------- /pkg/middleware/access_log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // AccessLog record access log. 11 | func AccessLog() gin.HandlerFunc { 12 | return gin.LoggerWithConfig( 13 | gin.LoggerConfig{ 14 | Formatter: func(params gin.LogFormatterParams) string { 15 | return fmt.Sprintf("%s - [%s] \"%s %s %s\" %d %d %s \"%s\" \"%s\"\n", 16 | params.ClientIP, 17 | params.TimeStamp.Format(time.RFC1123Z), 18 | params.Method, 19 | params.Path, 20 | params.Request.Proto, 21 | params.StatusCode, 22 | params.BodySize, 23 | params.Latency, 24 | params.Request.UserAgent(), 25 | params.ErrorMessage, 26 | ) 27 | }, 28 | SkipPaths: []string{"/health", "/metrics"}, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/go-eagle/eagle/pkg/app" 10 | "github.com/go-eagle/eagle/pkg/errcode" 11 | ) 12 | 13 | // Auth authorize user 14 | func Auth(paths ...string) gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | // ignore some path 17 | // eg: register, login, logout 18 | if len(paths) > 0 { 19 | path := c.Request.URL.Path 20 | pathsStr := strings.Join(paths, "|") 21 | reg := regexp.MustCompile("(" + pathsStr + ")") 22 | if reg.MatchString(path) { 23 | return 24 | } 25 | } 26 | 27 | // Parse the json web token. 28 | ctx, err := app.ParseRequest(c) 29 | if err != nil { 30 | app.NewResponse().Error(c, errcode.ErrInvalidToken) 31 | c.Abort() 32 | return 33 | } 34 | 35 | // set uid to context 36 | c.Set("uid", ctx.UserID) 37 | 38 | c.Next() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | const ( 11 | maxAge = 12 12 | ) 13 | 14 | // Cors add cors headers. 15 | func Cors() gin.HandlerFunc { 16 | return cors.New(cors.Config{ 17 | AllowOrigins: []string{"*"}, 18 | AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "OPTIONS", "DELETE"}, 19 | AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "Accept"}, 20 | ExposeHeaders: []string{"Content-Length"}, 21 | AllowCredentials: true, 22 | MaxAge: maxAge * time.Hour, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/middleware/header.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // NoCache is a middleware function that appends headers 11 | // to prevent the client from caching the HTTP response. 12 | func NoCache(c *gin.Context) { 13 | c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") 14 | c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") 15 | c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 16 | c.Next() 17 | } 18 | 19 | // Options is a middleware function that appends headers 20 | // for options requests and aborts then exits the middleware 21 | // chain and ends the request. 22 | func Options(c *gin.Context) { 23 | if c.Request.Method != "OPTIONS" { 24 | c.Next() 25 | } else { 26 | c.Header("Access-Control-Allow-Origin", "*") 27 | c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 28 | c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 29 | c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 30 | c.Header("Content-Type", "application/json") 31 | c.AbortWithStatus(200) 32 | } 33 | } 34 | 35 | // Secure is a middleware function that appends security 36 | // and resource access headers. 37 | func Secure(c *gin.Context) { 38 | c.Header("Access-Control-Allow-Origin", "*") 39 | c.Header("X-Frame-Options", "DENY") 40 | c.Header("X-Content-Type-Options", "nosniff") 41 | c.Header("X-XSS-Protection", "1; mode=block") 42 | if c.Request.TLS != nil { 43 | c.Header("Strict-Transport-Security", "max-age=31536000") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // Middlewares global middleware 6 | var Middlewares = defaultMiddlewares() 7 | 8 | func defaultMiddlewares() map[string]gin.HandlerFunc { 9 | return map[string]gin.HandlerFunc{ 10 | "recovery": gin.Recovery(), 11 | "secure": Secure, 12 | "options": Options, 13 | "nocache": NoCache, 14 | "logger": Logging(), 15 | "cors": Cors(), 16 | "request_id": RequestID(), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/middleware/ratelimit.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-kratos/aegis/ratelimit" 6 | "github.com/go-kratos/aegis/ratelimit/bbr" 7 | "github.com/pkg/errors" 8 | 9 | "github.com/go-eagle/eagle/pkg/app" 10 | ) 11 | 12 | // ErrLimitExceed is service unavailable due to rate limit exceeded. 13 | var ErrLimitExceed = errors.New("[RATELIMIT] service unavailable due to rate limit exceeded") 14 | 15 | // LimiterOption is ratelimit option. 16 | type LimiterOption func(*limiterOptions) 17 | 18 | // WithLimiter set Limiter implementation, 19 | // default is bbr limiter 20 | func WithLimiter(limiter ratelimit.Limiter) LimiterOption { 21 | return func(o *limiterOptions) { 22 | o.limiter = limiter 23 | } 24 | } 25 | 26 | type limiterOptions struct { 27 | limiter ratelimit.Limiter 28 | } 29 | 30 | // Ratelimit a circuit breaker middleware 31 | func Ratelimit(opts ...LimiterOption) gin.HandlerFunc { 32 | options := &limiterOptions{ 33 | limiter: bbr.NewLimiter(), 34 | } 35 | for _, o := range opts { 36 | o(options) 37 | } 38 | return func(c *gin.Context) { 39 | done, e := options.limiter.Allow() 40 | if e != nil { 41 | // rejected 42 | app.NewResponse().Error(c, ErrLimitExceed) 43 | c.Abort() 44 | return 45 | } 46 | // allowed 47 | done(ratelimit.DoneInfo{Err: c.Request.Context().Err()}) 48 | 49 | c.Next() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/middleware/timeout.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | timeout "github.com/vearne/gin-timeout" 8 | ) 9 | 10 | // Timeout 超时中间件 11 | func Timeout(t time.Duration) gin.HandlerFunc { 12 | // see: 13 | // https://github.com/vearne/gin-timeout 14 | // https://vearne.cc/archives/39135 15 | // https://github.com/gin-contrib/timeout 16 | return timeout.Timeout( 17 | timeout.WithTimeout(t), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/queue/README.md: -------------------------------------------------------------------------------- 1 | ## 消息队列 2 | 3 | - RocketMQ 4 | - RabbitMQ 5 | - Kafka 6 | - Nats 7 | 8 | 9 | ## 作用 10 | 11 | - 系统解耦 12 | - 异步处理 13 | - 削峰填谷 14 | 15 | ## 客户端 16 | 17 | - RocketMQ Go客户端: https://github.com/apache/rocketmq-client-go 18 | - kafka-go: https://github.com/segmentio/kafka-go 19 | - Nats.go: github.com/nats-io/nats.go 20 | 21 | > 如果是阿里云RocketMQ: 可以使用官方自己出的库 22 | 23 | 24 | ## 注意事项 25 | 26 | - Consumer 因可能多次收到同一消息,需要做好幂等处理 27 | - 消费时记录日志,方便后续定位问题,最好加上请求的唯一标识,比如 request_id或trace_id之类的字段 28 | - 尽量使用批量方式消费,可以很大程度上提高消费吞吐量 29 | 30 | 31 | ## Reference 32 | 33 | - [RocketMQ官网](https://rocketmq.apache.org/) 34 | - [RocketMQ文档](https://rocketmq.apache.org/docs/quick-start/) 35 | - [RocketMQ Go客户端](https://github.com/apache/rocketmq-client-go) 36 | - [RocketMQ Go客户端使用文档](https://github.com/apache/rocketmq-client-go/blob/master/docs/Introduction.md) 37 | - [阿里云RocketMQ](https://cn.aliyun.com/product/rocketmq) 38 | - https://github.com/GSabadini/go-message-broker/blob/master/main.go 39 | - [Automatically recovering RabbitMQ connections in Go applications](https://medium.com/@dhanushgopinath/automatically-recovering-rabbitmq-connections-in-go-applications-7795a605ca59) 40 | - [https://github.com/wagslane/go-rabbitmq](https://github.com/wagslane/go-rabbitmq) 41 | -------------------------------------------------------------------------------- /pkg/queue/interface.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | // Producer queue producer 4 | type Producer interface { 5 | Publish(message string) error 6 | } 7 | 8 | // Consumer queue consumer 9 | type Consumer interface { 10 | Consume() error 11 | } 12 | -------------------------------------------------------------------------------- /pkg/queue/kafka/client.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | // Publish add data to queue 10 | func Publish(ctx context.Context, name string, topic, msg string) error { 11 | p, err := DefaultManager.GetProducer(name) 12 | if err != nil { 13 | return err 14 | } 15 | return p.Publish(ctx, topic, msg) 16 | } 17 | 18 | // Consume data from queue 19 | func Consume(ctx context.Context, name string, topics []string, handler sarama.ConsumerGroupHandler) error { 20 | c, err := DefaultManager.GetConsumer(name) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return c.Consume(ctx, topics, handler) 26 | } 27 | 28 | // ConsumeByPartition consume data by partition 29 | func ConsumePartition(ctx context.Context, name, topic string, handler func([]byte) error) error { 30 | c, err := DefaultManager.GetConsumer(name) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return c.ConsumePartition(ctx, topic, handler) 36 | } 37 | 38 | // ConsumerByPartitionId consume data by partition id 39 | func ConsumerByPartitionId(ctx context.Context, name, topic string, partition int32, handler func([]byte) error) error { 40 | c, err := DefaultManager.GetConsumer(name) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return c.ConsumerByPartitionId(ctx, topic, partition, handler) 46 | } 47 | 48 | // GetPartitionList get partition list 49 | func GetPartitionList(ctx context.Context, name, topic string) ([]int32, error) { 50 | c, err := DefaultManager.GetConsumer(name) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return c.client.Partitions(topic) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/queue/kafka/config.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "sync" 5 | 6 | cfg "github.com/go-eagle/eagle/pkg/config" 7 | ) 8 | 9 | var ( 10 | loadOnce sync.Once 11 | closeOnce sync.Once 12 | conf map[string]*Conf 13 | ) 14 | 15 | type Conf struct { 16 | Version string `yaml:"Version"` 17 | RequiredAcks int `yaml:"RequiredAcks"` 18 | Topic string `yaml:"Topic"` 19 | ConsumeTopic []string `yaml:"VonsumeTopic"` 20 | Brokers []string `yaml:"Brokers"` 21 | GroupID string `yaml:"GroupID"` 22 | // partitioner type,optional: "random", "roundrobin", "hash" 23 | Partitioner string `yaml:"Partitioner"` 24 | } 25 | 26 | // loadConf load config 27 | func loadConf() (ret map[string]*Conf, err error) { 28 | v, err := cfg.LoadWithType("kafka", "yaml") 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | c := make(map[string]*Conf, 0) 34 | err = v.Unmarshal(&c) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | conf = c 40 | 41 | return c, nil 42 | } 43 | 44 | func GetConfig() map[string]*Conf { 45 | return conf 46 | } 47 | 48 | func Load() { 49 | loadOnce.Do(func() { 50 | conf, err := loadConf() 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | DefaultManager = NewManager(conf) 56 | }) 57 | } 58 | 59 | func Close() { 60 | closeOnce.Do(func() { 61 | _ = DefaultManager.Close() 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/queue/kafka/consumer_handler.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/IBM/sarama" 8 | ) 9 | 10 | // ConsumerGroupHandler represents the sarama consumer group 11 | type ConsumerGroupHandler struct{} 12 | 13 | // Setup is run before consumer start consuming, is normally used to setup things such as database connections 14 | func (h ConsumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { 15 | return nil 16 | } 17 | 18 | // Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited 19 | func (h ConsumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { 20 | return nil 21 | } 22 | 23 | // ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages(), here is supposed to be what you want to 24 | // do with the message. In this example the message will be logged with the topic name, partition and message value. 25 | func (h ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { 26 | for msg := range claim.Messages() { 27 | fmt.Printf( 28 | "Message topic:%q partition:%d offset:%d message: %v\n", 29 | msg.Topic, msg.Partition, msg.Offset, string(msg.Value), 30 | ) 31 | 32 | session.MarkMessage(msg, "") 33 | 34 | log.Printf(" [*] Waiting for messages. To exit press CTRL+C") 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/queue/nats/init.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | var ( 4 | // Queue nats queue 5 | Queue *Producer 6 | ) 7 | 8 | // Config nats config 9 | type Config struct { 10 | Addr string `mapstructure:"name"` 11 | } 12 | 13 | // Init create producer 14 | func Init(cfg *Config) { 15 | Queue = NewProducer(cfg.Addr) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/queue/rabbitmq/client.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-eagle/eagle/pkg/queue/rabbitmq/options" 8 | ) 9 | 10 | // Publish add data to queue 11 | func Publish(ctx context.Context, name string, data []byte, opts ...options.PublishOption) error { 12 | p, err := DefaultManager.GetProducer(name) 13 | if err != nil { 14 | return err 15 | } 16 | return p.Publish(ctx, data, opts...) 17 | } 18 | 19 | // PublishWithDelay add a delay msg to queue 20 | // delayTime: seconds 21 | func PublishWithDelay(ctx context.Context, name string, data []byte, delayTime int, opts ...options.PublishOption) error { 22 | p, err := DefaultManager.GetProducer(name) 23 | if err != nil { 24 | return err 25 | } 26 | opts = append(opts, options.WithPublishOptionHeaders(map[string]interface{}{ 27 | "x-delay": delayTime * 1000, // seconds 28 | })) 29 | return p.Publish(ctx, data, opts...) 30 | } 31 | 32 | // Consume data from queue 33 | func Consume(ctx context.Context, name string, handler Handler, opts ...options.ConsumerOption) error { 34 | c, err := DefaultManager.GetConsumer(name) 35 | if err != nil { 36 | return err 37 | } 38 | if c.IsClosed() { 39 | return fmt.Errorf("rabbitmq: consumer %s closed", name) 40 | } 41 | return c.Consume(ctx, handler, opts...) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/queue/rabbitmq/options/bind.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | type BindOptions struct { 4 | RoutingKey string `yaml:"routing-key"` 5 | NoWait bool `yaml:"no-wait"` 6 | Args map[string]interface{} `yaml:"args"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/queue/rabbitmq/options/exchange.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | type ExchangeOptions struct { 4 | Name string `yaml:"name"` 5 | Kind string `yaml:"kind"` 6 | Durable bool `yaml:"durable"` 7 | AutoDelete bool `yaml:"auto-delete"` 8 | Internal bool `yaml:"internal"` 9 | NoWait bool `yaml:"no-wait"` 10 | Args map[string]interface{} `yaml:"args"` 11 | } 12 | -------------------------------------------------------------------------------- /pkg/queue/rabbitmq/options/queue.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | type QueueOptions struct { 4 | Name string `yaml:"name"` 5 | Durable bool `yaml:"durable"` 6 | AutoDelete bool `yaml:"auto-delete"` 7 | Exclusive bool `yaml:"exclusive"` 8 | NoWait bool `yaml:"no-wait"` 9 | Args map[string]interface{} `yaml:"args"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/redis/README.md: -------------------------------------------------------------------------------- 1 | 2 | 单元测试可以使用 https://github.com/alicebob/miniredis, 可以开启一个本地的模拟redis 3 | 4 | - [在单元测试中模拟Redis](https://medium.com/@elliotchance/mocking-redis-in-unit-tests-in-go-28aff285b98) 5 | 6 | ## 案例 7 | 8 | - [Redis分布式锁没用明白,搞出了大故障…](https://mp.weixin.qq.com/s/BO-gly5iGLVmuG5B_FIpoQ) 9 | - [看完这篇Redis缓存三大问题](https://mp.weixin.qq.com/s/HjzwefprYSGraU1aJcJ25g) 10 | 11 | ## Redis 优化方向 12 | 13 | ### 参数优化 14 | 15 | maxIdle设置高点,可以保证突发流量情况下,能够有足够的连接去获取redis,不用在高流量情况下建立连接 16 | 17 | **go-redis参数优化** 18 | 19 | ```yaml 20 | min_idle_conn: 30 21 | dial_timeout: "1s" 22 | read_timeout: "500ms" 23 | write_timeout: "500ms" 24 | pool_size: 500 25 | pool_timeout: "60s" 26 | ``` 27 | 28 | **redisgo参数优化** 29 | 30 | ```yaml 31 | maxIdle = 30 32 | maxActive = 500 33 | dialTimeout = "1s" 34 | readTimeout = "500ms" 35 | writeTimeout = "500ms" 36 | idleTimeout = "60s" 37 | ``` 38 | 39 | ### 使用优化 40 | 41 | - 增加redis从库 42 | - 对批量数据,根据redis从库数量,并发goroutine拉取数据 43 | - 对批量数据大量使用pipeline指令 44 | - 精简key字段 45 | - redis的value存储编解码改为msgpack 46 | 47 | ## Pipeline 48 | - https://redis.io/topics/pipelining 49 | - [兼容go redis cluster的pipeline批量](http://xiaorui.cc/archives/5557) 50 | - https://www.tizi365.com/archives/309.html -------------------------------------------------------------------------------- /pkg/redis/config.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "time" 4 | 5 | // Config redis config 6 | type Config struct { 7 | Addr string 8 | Password string 9 | DB int 10 | MinIdleConn int 11 | DialTimeout time.Duration 12 | ReadTimeout time.Duration 13 | WriteTimeout time.Duration 14 | PoolSize int 15 | PoolTimeout time.Duration 16 | // tracing switch 17 | EnableTrace bool 18 | } 19 | -------------------------------------------------------------------------------- /pkg/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestInit(t *testing.T) { 12 | InitTestRedis() 13 | 14 | // test init 15 | ret, err := RedisClient.Ping(context.Background()).Result() 16 | assert.Nil(t, err) 17 | assert.Equal(t, "PONG", ret) 18 | } 19 | 20 | func TestNewRedisClient(t *testing.T) { 21 | // test default client 22 | InitTestRedis() 23 | client := NewRedisManager() 24 | assert.NotNil(t, client) 25 | 26 | // get a not exist client 27 | // rdb, err := client.GetClient("not-exist") 28 | // assert.NotNil(t, err) 29 | // assert.Nil(t, rdb) 30 | } 31 | 32 | func TestRedisSetGet(t *testing.T) { 33 | InitTestRedis() 34 | 35 | var setGetKey = "test-set" 36 | var setGetValue = "test-content" 37 | RedisClient.Set(context.Background(), setGetKey, setGetValue, time.Second*100) 38 | 39 | expectValue := RedisClient.Get(context.Background(), setGetKey).Val() 40 | assert.Equal(t, setGetValue, expectValue) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/registry/consul/service.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | "github.com/go-eagle/eagle/pkg/registry" 8 | ) 9 | 10 | type serviceSet struct { 11 | serviceName string 12 | watcher map[*watcher]struct{} 13 | services *atomic.Value 14 | lock sync.RWMutex 15 | } 16 | 17 | func (s *serviceSet) broadcast(ss []*registry.ServiceInstance) { 18 | s.services.Store(ss) 19 | s.lock.RLock() 20 | defer s.lock.RUnlock() 21 | for k := range s.watcher { 22 | select { 23 | case k.event <- struct{}{}: 24 | default: 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/registry/consul/watcher.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-eagle/eagle/pkg/registry" 7 | ) 8 | 9 | type watcher struct { 10 | event chan struct{} 11 | set *serviceSet 12 | 13 | // for cancel 14 | ctx context.Context 15 | cancel context.CancelFunc 16 | } 17 | 18 | func (w *watcher) Next() (services []*registry.ServiceInstance, err error) { 19 | select { 20 | case <-w.ctx.Done(): 21 | err = w.ctx.Err() 22 | case <-w.event: 23 | } 24 | 25 | ss, ok := w.set.services.Load().([]*registry.ServiceInstance) 26 | 27 | if ok { 28 | services = append(services, ss...) 29 | } 30 | return 31 | } 32 | 33 | func (w *watcher) Stop() error { 34 | w.cancel() 35 | w.set.lock.Lock() 36 | defer w.set.lock.Unlock() 37 | delete(w.set.watcher, w) 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/registry/etcd/service.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/go-eagle/eagle/pkg/registry" 7 | ) 8 | 9 | func marshal(si *registry.ServiceInstance) (string, error) { 10 | data, err := json.Marshal(si) 11 | if err != nil { 12 | return "", err 13 | } 14 | return string(data), nil 15 | } 16 | 17 | func unmarshal(data []byte) (si *registry.ServiceInstance, err error) { 18 | err = json.Unmarshal(data, &si) 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /pkg/sign/keys.go: -------------------------------------------------------------------------------- 1 | package sign 2 | 3 | const ( 4 | // KeyNameTimeStamp 时间戳字段名 5 | KeyNameTimeStamp = "timestamp" 6 | // KeyNameNonceStr temp str field 7 | KeyNameNonceStr = "nonce_str" 8 | // KeyNameAppID app id field 9 | KeyNameAppID = "app_id" 10 | // KeyNameSign sign field 11 | KeyNameSign = "sign" 12 | ) 13 | 14 | // DefaultKeyName 签名需要用到的字段 15 | type DefaultKeyName struct { 16 | Timestamp string 17 | NonceStr string 18 | AppID string 19 | Sign string 20 | } 21 | 22 | func newDefaultKeyName() *DefaultKeyName { 23 | return &DefaultKeyName{ 24 | Timestamp: KeyNameTimeStamp, 25 | NonceStr: KeyNameNonceStr, 26 | AppID: KeyNameAppID, 27 | Sign: KeyNameSign, 28 | } 29 | } 30 | 31 | // SetKeyNameTimestamp 设定时间戳 32 | func (d *DefaultKeyName) SetKeyNameTimestamp(name string) { 33 | d.Timestamp = name 34 | } 35 | 36 | // SetKeyNameNonceStr 设定随机字符串 37 | func (d *DefaultKeyName) SetKeyNameNonceStr(name string) { 38 | d.NonceStr = name 39 | } 40 | 41 | // SetKeyNameAppID 设定app id 42 | func (d *DefaultKeyName) SetKeyNameAppID(name string) { 43 | d.AppID = name 44 | } 45 | 46 | // SetKeyNameSign 设定签名 47 | func (d *DefaultKeyName) SetKeyNameSign(name string) { 48 | d.Sign = name 49 | } 50 | -------------------------------------------------------------------------------- /pkg/sign/sign_hmac.go: -------------------------------------------------------------------------------- 1 | package sign 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | ) 7 | 8 | // HmacSign hmac 9 | func HmacSign(secretKey, body string) []byte { 10 | m := hmac.New(sha1.New, []byte(secretKey)) 11 | _, _ = m.Write([]byte(body)) 12 | return m.Sum(nil) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/sign/sign_md5.go: -------------------------------------------------------------------------------- 1 | package sign 2 | 3 | import "crypto/md5" 4 | 5 | // Md5Sign md5 sign 6 | func Md5Sign(_, body string) []byte { 7 | m := md5.New() 8 | _, _ = m.Write([]byte(body)) 9 | return m.Sum(nil) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/sign/verifer_test.go: -------------------------------------------------------------------------------- 1 | package sign 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestVerify_ParseQuery(t *testing.T) { 9 | requestURI := "/restful/api/numbers?app_id=9d8a121ce581499d&nonce_str=tempstring&city=beijing" + 10 | "×tamp=1532585241&sign=0f5b8c97920bc95f1a8b893f41b42d9e" 11 | 12 | // 第一步:创建Verify校验类 13 | verifier := NewVerifier() 14 | 15 | // 假定从RequestUri中读取校验参数 16 | if err := verifier.ParseQuery(requestURI); nil != err { 17 | t.Fatal(err) 18 | } 19 | 20 | // 第二步:(可选)校验是否包含签名校验必要的参数 21 | if err := verifier.MustHasOtherKeys("city"); nil != err { 22 | t.Fatal(err) 23 | } 24 | 25 | // 第三步:检查时间戳是否超时。 26 | //if err := verifier.CheckTimeStamp(); nil != err { 27 | // t.Fatal(err) 28 | //} 29 | 30 | // 第四步,创建Sign来重现客户端的签名信息: 31 | signer := NewSignerMd5() 32 | 33 | // 第五步:从Verify中读取所有请求参数 34 | signer.SetBody(verifier.GetBodyWithoutSign()) 35 | 36 | // 第六步:从数据库读取AppID对应的SecretKey 37 | // appId := verifier.MustString("app_id") 38 | secretKey := "d93047a4d6fe6111" 39 | 40 | // 使用同样的WrapBody方式 41 | signer.SetAppSecretWrapBody(secretKey) 42 | 43 | // 生成 44 | sign := signer.GetSignature() 45 | t.Log("sign", sign) 46 | 47 | // 校验自己生成的和传递过来的是否一致 48 | if verifier.MustString("sign") != sign { 49 | t.Fatal("校验失败") 50 | } 51 | 52 | fmt.Println(sign) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/storage/elasticsearch/README.md: -------------------------------------------------------------------------------- 1 | # 主要是提供一些搜索服务客户端,比如常见的 Elasticsearch、solr 2 | 3 | 4 | ### Elasticsearch 5 | 6 | Elasticsearch 是当前比较流行的 搜索框架,这里提供两个库,第三方和官方的,特点各有千秋。 7 | 8 | - 官方 [https://github.com/elastic/go-elasticsearch](https://github.com/elastic/go-elasticsearch) star: 2700+ 9 | - 第三方[https://github.com/olivere/elastic](https://github.com/olivere/elastic) star: 5300+ 10 | 11 | > 第三方的这个已经不在维护,所以推荐使用官方的 12 | 13 | ## Compatibility 14 | 15 | The client major versions correspond to the compatible Elasticsearch major versions: to connect to Elasticsearch 7.x, 16 | use a 7.x version of the client, to connect to Elasticsearch 6.x, use a 6.x version of the client. 17 | 18 | ## Reference 19 | 20 | - https://medium.com/a-journey-with-go/go-elasticsearch-clients-study-case-dbaee1e02c7 21 | - https://mp.weixin.qq.com/s?__biz=MzkyNzI1NzM5NQ==&mid=2247484756&idx=1&sn=6029ed72391e175ebef3de47bf9290a2&chksm=c22b8308f55c0a1eb0d06f4f5f7404e0ba33f6bb10a86f996dfbbd7b80b952e5bef97ce2344f&cur_album_id=1932372564102709257&scene=189#wechat_redirect 22 | - https://discuss.elastic.co/t/go-elasticsearch-versus-olivere-golang-client/252248 23 | - https://github.com/olivere/elastic/issues/1240 24 | - https://www.elastic.co/guide/en/elasticsearch/reference/7.17/docker.html 25 | - https://studygolang.com/articles/28869 -------------------------------------------------------------------------------- /pkg/storage/elasticsearch/index.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | ) 7 | 8 | // CreateIndex creates an index with the given name and body. 9 | func (es *EsClient) CreateIndex(ctx context.Context, indexName string, body string) (map[string]interface{}, error) { 10 | resp, err := es.client.Indices.Create(indexName, 11 | es.client.Indices.Create.WithContext(ctx), 12 | es.client.Indices.Create.WithBody(strings.NewReader(body))) 13 | if err != nil { 14 | return nil, err 15 | } 16 | defer resp.Body.Close() 17 | 18 | return es.handleResponse(resp) 19 | } 20 | 21 | // GetIndex gets an index with the given name. 22 | func (es *EsClient) GetIndex(ctx context.Context, indexName string) (map[string]interface{}, error) { 23 | resp, err := es.client.Indices.Get([]string{indexName}, es.client.Indices.Get.WithContext(ctx)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | defer resp.Body.Close() 28 | 29 | return es.handleResponse(resp) 30 | } 31 | 32 | // DeleteIndex deletes an index with the given name. 33 | func (es *EsClient) DeleteIndex(ctx context.Context, indexName string) error { 34 | resp, err := es.client.Indices.Delete([]string{indexName}, es.client.Indices.Delete.WithContext(ctx)) 35 | if err != nil { 36 | return err 37 | } 38 | defer resp.Body.Close() 39 | 40 | _, err = es.handleResponse(resp) 41 | return err 42 | } 43 | -------------------------------------------------------------------------------- /pkg/storage/mongodb/mongodb.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.mongodb.org/mongo-driver/mongo" 8 | "go.mongodb.org/mongo-driver/mongo/options" 9 | ) 10 | 11 | const ( 12 | connectTimeout = 30 * time.Second 13 | maxConnIdleTime = 3 * time.Minute 14 | minPoolSize = 20 15 | maxPoolSize = 300 16 | ) 17 | 18 | // Config MongoDB config 19 | type Config struct { 20 | URI string 21 | User string 22 | Password string 23 | DB string 24 | } 25 | 26 | // NewMongoDBConn Create new MongoDB client 27 | func NewMongoDBConn(ctx context.Context, cfg *Config) (*mongo.Client, error) { 28 | client, err := mongo.NewClient( 29 | options.Client().ApplyURI(cfg.URI). 30 | SetAuth(options.Credential{ 31 | Username: cfg.User, 32 | Password: cfg.Password, 33 | }). 34 | SetConnectTimeout(connectTimeout). 35 | SetMaxConnIdleTime(maxConnIdleTime). 36 | SetMinPoolSize(minPoolSize). 37 | SetMaxPoolSize(maxPoolSize)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if err := client.Connect(ctx); err != nil { 43 | return nil, err 44 | } 45 | 46 | if err := client.Ping(ctx, nil); err != nil { 47 | return nil, err 48 | } 49 | 50 | return client, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/storage/orm/log_writer.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-eagle/eagle/pkg/log" 7 | "gorm.io/gorm/logger" 8 | ) 9 | 10 | type LoggerWriter struct { 11 | log log.Logger 12 | } 13 | 14 | func NewLogWriter(log log.Logger) logger.Writer { 15 | return &LoggerWriter{ 16 | log: log, 17 | } 18 | } 19 | 20 | func (l *LoggerWriter) Printf(s string, v ...interface{}) { 21 | l.log.Info(fmt.Sprintf(s, v...)) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/storage/sql/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-eagle/eagle/1b5c80f19311299eb51c4f8c4f1dba7adf0e2210/pkg/storage/sql/README.md -------------------------------------------------------------------------------- /pkg/storage/sql/metrics.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "github.com/go-eagle/eagle/pkg/metric" 5 | ) 6 | 7 | const namespace = "mysql_client" 8 | 9 | // nolint 10 | var ( 11 | _metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ 12 | Namespace: namespace, 13 | Subsystem: "requests", 14 | Name: "duration_ms", 15 | Help: "mysql client requests duration(ms).", 16 | Labels: []string{"name", "addr", "command"}, 17 | Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500}, 18 | }) 19 | _metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{ 20 | Namespace: namespace, 21 | Subsystem: "requests", 22 | Name: "error_total", 23 | Help: "mysql client requests error count.", 24 | Labels: []string{"name", "addr", "command", "error"}, 25 | }) 26 | _metricConnTotal = metric.NewCounterVec(&metric.CounterVecOpts{ 27 | Namespace: namespace, 28 | Subsystem: "connections", 29 | Name: "total", 30 | Help: "mysql client connections total count.", 31 | Labels: []string{"name", "addr", "state"}, 32 | }) 33 | _metricConnCurrent = metric.NewGaugeVec(&metric.GaugeVecOpts{ 34 | Namespace: namespace, 35 | Subsystem: "connections", 36 | Name: "current", 37 | Help: "mysql client connections current.", 38 | Labels: []string{"name", "addr", "state"}, 39 | }) 40 | ) 41 | -------------------------------------------------------------------------------- /pkg/storage/sql/row.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | "go.opentelemetry.io/otel/trace" 10 | ) 11 | 12 | // Row row. 13 | type Row struct { 14 | err error 15 | *sql.Row 16 | db *conn 17 | query string 18 | args []interface{} 19 | trace trace.Tracer 20 | cancel func() 21 | } 22 | 23 | // Scan copies the columns from the matched row into the values pointed at by dest. 24 | func (r *Row) Scan(dest ...interface{}) (err error) { 25 | defer slowLog(fmt.Sprintf("Scan query: %s args: %+v", r.query, r.args), time.Now()) 26 | if r.trace != nil { 27 | //r.trace. 28 | //defer r.trace.Finish(&err) 29 | } 30 | if r.err != nil { 31 | err = r.err 32 | } else if r.Row == nil { 33 | err = ErrStmtNil 34 | } 35 | if err != nil { 36 | return 37 | } 38 | err = r.Row.Scan(dest...) 39 | if r.cancel != nil { 40 | r.cancel() 41 | } 42 | r.db.onBreaker(&err) 43 | if err != ErrNoRows { 44 | err = errors.Wrapf(err, "query: %s, args: %+v", r.query, r.args) 45 | } 46 | return 47 | } 48 | 49 | // Rows rows. 50 | type Rows struct { 51 | *sql.Rows 52 | cancel func() 53 | } 54 | 55 | // Close closes the Rows, preventing further enumeration. If Next is called 56 | // and returns false and there are no further result sets, 57 | // the Rows are closed automatically and it will suffice to check the 58 | // result of Err. Close is idempotent and does not affect the result of Err. 59 | func (rs *Rows) Close() (err error) { 60 | err = errors.WithStack(rs.Rows.Close()) 61 | if rs.cancel != nil { 62 | rs.cancel() 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /pkg/sync/errgroup/doc.go: -------------------------------------------------------------------------------- 1 | // Package errgroup provides synchronization, error propagation, and Context 2 | // errgroup 包为一组子任务的 goroutine 提供了 goroutine 同步,错误取消功能. 3 | // 4 | //errgroup 包含三种常用方式 5 | // 6 | //1、直接使用 此时不会因为一个任务失败导致所有任务被 cancel: 7 | // g := &errgroup.Group{} 8 | // g.Go(func(ctx context.Context) { 9 | // // NOTE: 此时 ctx 为 context.Background() 10 | // // do something 11 | // }) 12 | // 13 | //2、WithContext 使用 WithContext 时不会因为一个任务失败导致所有任务被 cancel: 14 | // g := errgroup.WithContext(ctx) 15 | // g.Go(func(ctx context.Context) { 16 | // // NOTE: 此时 ctx 为 errgroup.WithContext 传递的 ctx 17 | // // do something 18 | // }) 19 | // 20 | //3、WithCancel 使用 WithCancel 时如果有一个人任务失败会导致所有*未进行或进行中*的任务被 cancel: 21 | // g := errgroup.WithCancel(ctx) 22 | // g.Go(func(ctx context.Context) { 23 | // // NOTE: 此时 ctx 是从 errgroup.WithContext 传递的 ctx 派生出的 ctx 24 | // // do something 25 | // }) 26 | // 27 | //设置最大并行数 GOMAXPROCS 对以上三种使用方式均起效 28 | //NOTE: 由于 errgroup 实现问题,设定 GOMAXPROCS 的 errgroup 需要立即调用 Wait() 例如: 29 | // 30 | // g := errgroup.WithCancel(ctx) 31 | // g.GOMAXPROCS(2) 32 | // // task1 33 | // g.Go(func(ctx context.Context) { 34 | // fmt.Println("task1") 35 | // }) 36 | // // task2 37 | // g.Go(func(ctx context.Context) { 38 | // fmt.Println("task2") 39 | // }) 40 | // // task3 41 | // g.Go(func(ctx context.Context) { 42 | // fmt.Println("task3") 43 | // }) 44 | // // NOTE: 此时设置的 GOMAXPROCS 为2, 添加了三个任务 task1, task2, task3 此时 task3 是不会运行的! 45 | // // 只有调用了 Wait task3 才有运行的机会 46 | // g.Wait() // task3 运行 47 | package errgroup 48 | -------------------------------------------------------------------------------- /pkg/sync/errgroup/example_test.go: -------------------------------------------------------------------------------- 1 | package errgroup 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func fakeRunTask(ctx context.Context) error { 8 | return nil 9 | } 10 | 11 | func ExampleGroup_group() { 12 | g := Group{} 13 | g.Go(func(context.Context) error { 14 | return fakeRunTask(context.Background()) 15 | }) 16 | g.Go(func(context.Context) error { 17 | return fakeRunTask(context.Background()) 18 | }) 19 | if err := g.Wait(); err != nil { 20 | // handle err 21 | } 22 | } 23 | 24 | func ExampleGroup_ctx() { 25 | g := WithContext(context.Background()) 26 | g.Go(func(ctx context.Context) error { 27 | return fakeRunTask(ctx) 28 | }) 29 | g.Go(func(ctx context.Context) error { 30 | return fakeRunTask(ctx) 31 | }) 32 | if err := g.Wait(); err != nil { 33 | // handle err 34 | } 35 | } 36 | 37 | func ExampleGroup_cancel() { 38 | g := WithCancel(context.Background()) 39 | g.Go(func(ctx context.Context) error { 40 | return fakeRunTask(ctx) 41 | }) 42 | g.Go(func(ctx context.Context) error { 43 | return fakeRunTask(ctx) 44 | }) 45 | if err := g.Wait(); err != nil { 46 | // handle err 47 | } 48 | } 49 | 50 | func ExampleGroup_maxproc() { 51 | g := Group{} 52 | // set max concurrency 53 | g.GOMAXPROCS(2) 54 | g.Go(func(ctx context.Context) error { 55 | return fakeRunTask(context.Background()) 56 | }) 57 | g.Go(func(ctx context.Context) error { 58 | return fakeRunTask(context.Background()) 59 | }) 60 | if err := g.Wait(); err != nil { 61 | // handle err 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/sync/pipeline/fanout/example_test.go: -------------------------------------------------------------------------------- 1 | package fanout 2 | 3 | import "context" 4 | 5 | // addCache 加缓存的例子 6 | func addCache(c context.Context, id, value int) { 7 | // some thing... 8 | } 9 | 10 | func Example() { 11 | var c = context.Background() 12 | // 新建一个fanout 对象 名称为cache 13 | // (可选参数) worker数量为1 表示后台只有1个线程在工作 14 | // (可选参数) buffer 为1024 表示缓存chan长度为1024 如果chan满了 再调用Do方法就会报错 设定长度主要为了防止OOM 15 | cache := New("cache", Worker(1), Buffer(1024)) 16 | // 需要异步执行的方法 17 | // 这里传进来的c里面的meta信息会被复制 超时会忽略 addCache拿到的context已经没有超时信息了 18 | cache.Do(c, func(c context.Context) { addCache(c, 0, 0) }) 19 | // 程序结束的时候关闭fanout 会等待后台线程完成后返回 20 | cache.Close() 21 | } 22 | -------------------------------------------------------------------------------- /pkg/sync/pipeline/fanout/fanout_test.go: -------------------------------------------------------------------------------- 1 | package fanout 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestFanout_Do(t *testing.T) { 10 | ca := New("cache", Worker(1), Buffer(1024)) 11 | var run bool 12 | ca.Do(context.Background(), func(c context.Context) { 13 | run = true 14 | panic("error") 15 | }) 16 | time.Sleep(time.Millisecond * 50) 17 | t.Log("not panic") 18 | if !run { 19 | t.Fatal("expect run be true") 20 | } 21 | } 22 | 23 | func TestFanout_Close(t *testing.T) { 24 | ca := New("cache", Worker(1), Buffer(1024)) 25 | ca.Close() 26 | err := ca.Do(context.Background(), func(c context.Context) {}) 27 | if err == nil { 28 | t.Fatal("expect get err") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/time/time.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "context" 5 | xtime "time" 6 | ) 7 | 8 | // Duration be used toml unmarshal string time, like 1s, 500ms. 9 | type Duration xtime.Duration 10 | 11 | // UnmarshalText unmarshal text to duration. 12 | func (d *Duration) UnmarshalText(text []byte) error { 13 | tmp, err := xtime.ParseDuration(string(text)) 14 | if err == nil { 15 | *d = Duration(tmp) 16 | } 17 | return err 18 | } 19 | 20 | // Shrink will decrease the duration by comparing with context's timeout duration 21 | // and return new timeout\context\CancelFunc. 22 | func (d Duration) Shrink(c context.Context) (Duration, context.Context, context.CancelFunc) { 23 | if deadline, ok := c.Deadline(); ok { 24 | if ctimeout := xtime.Until(deadline); ctimeout < xtime.Duration(d) { 25 | // deliver small timeout 26 | return Duration(ctimeout), c, func() {} 27 | } 28 | } 29 | ctx, cancel := context.WithTimeout(c, xtime.Duration(d)) 30 | return d, ctx, cancel 31 | } 32 | -------------------------------------------------------------------------------- /pkg/trace/config.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | // Config jaeger config 4 | type Config struct { 5 | ServiceName string // The name of this service 6 | LocalAgentHostPort string 7 | CollectorEndpoint string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/trace/errors.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/otel/codes" 7 | "go.opentelemetry.io/otel/trace" 8 | ) 9 | 10 | // SetSpanError record error to tracing system 11 | func SetSpanError(ctx context.Context, err error) { 12 | span := trace.SpanFromContext(ctx) 13 | 14 | if span.IsRecording() { 15 | span.RecordError(err) 16 | span.SetStatus(codes.Error, err.Error()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/trace/options.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | // Option is a function that sets some option on the client. 4 | type Option func(c *Options) 5 | 6 | // Options control behavior of the client. 7 | type Options struct { 8 | SamplingRatio float64 9 | } 10 | 11 | func applyOptions(options ...Option) Options { 12 | opts := Options{ 13 | 1, 14 | } 15 | for _, option := range options { 16 | option(&opts) 17 | } 18 | 19 | return opts 20 | } 21 | -------------------------------------------------------------------------------- /pkg/trace/plugins/function/tracer.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | "runtime" 8 | 9 | "go.opentelemetry.io/otel/attribute" 10 | 11 | "go.opentelemetry.io/contrib" 12 | "go.opentelemetry.io/otel" 13 | "go.opentelemetry.io/otel/trace" 14 | ) 15 | 16 | const ( 17 | // TraceName function trance name 18 | TraceName = "github.com/go-eagle/eagle/trace/plugins/function" 19 | // PluginName plugin category name 20 | PluginName = "function" 21 | ) 22 | 23 | var tracer trace.Tracer 24 | 25 | func init() { 26 | tracer = otel.GetTracerProvider().Tracer(TraceName, trace.WithInstrumentationVersion(contrib.SemVersion())) 27 | } 28 | 29 | // StartFromContext create a new context 30 | func StartFromContext(ctx context.Context) (context.Context, trace.Span) { 31 | var spanAttrs []trace.SpanStartOption 32 | 33 | fileName, lineNo, funcName := getCallerInfo(2) 34 | caller := fmt.Sprintf("FuncName: %s, file: %s, line: %d", funcName, fileName, lineNo) 35 | if caller != "" { 36 | spanAttrs = append(spanAttrs, trace.WithAttributes(attribute.String("caller", caller))) 37 | } 38 | 39 | spanName := fmt.Sprintf("%s: %s", PluginName, funcName) 40 | return tracer.Start(ctx, spanName, spanAttrs...) 41 | } 42 | 43 | func getCallerInfo(skip int) (string, int, string) { 44 | var ( 45 | fileName string 46 | lineNo int 47 | funcName string 48 | ) 49 | pc, file, lineNo, ok := runtime.Caller(skip) 50 | if !ok { 51 | return fileName, lineNo, funcName 52 | } 53 | 54 | funcName = runtime.FuncForPC(pc).Name() 55 | fileName = path.Base(file) 56 | 57 | return fileName, lineNo, funcName 58 | } 59 | -------------------------------------------------------------------------------- /pkg/transport/consumer/redis/options.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | // ServerOption is cron server option. 4 | type ServerOption func(o *Server) 5 | 6 | // WithAddress with server address. 7 | func WithAddress(addr string) ServerOption { 8 | return func(s *Server) { 9 | s.clientOpt.Addr = addr 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pkg/transport/consumer/redis/server.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/hibiken/asynq" 9 | ) 10 | 11 | const ( 12 | // QueueCritical queue priority 13 | QueueCritical = "critical" 14 | QueueDefault = "default" 15 | QueueLow = "low" 16 | ) 17 | 18 | // Server async server 19 | type Server struct { 20 | clientOpt asynq.RedisClientOpt 21 | 22 | // async server 23 | srv *asynq.Server 24 | mux *asynq.ServeMux 25 | } 26 | 27 | // NewServer new async server 28 | func NewServer(redisOpt asynq.RedisClientOpt, asyncCfg asynq.Config) *Server { 29 | srv := &Server{ 30 | srv: asynq.NewServer(redisOpt, asyncCfg), 31 | mux: asynq.NewServeMux(), 32 | } 33 | 34 | return srv 35 | } 36 | 37 | // Start async server 38 | func (s *Server) Start(ctx context.Context) error { 39 | err := s.srv.Run(s.mux) 40 | if err != nil { 41 | return errors.Wrapf(err, "failed to run async server") 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // Stop async server 48 | func (s *Server) Stop(ctx context.Context) error { 49 | s.srv.Shutdown() 50 | return nil 51 | } 52 | 53 | // RegisterHandler register handler 54 | func (s *Server) RegisterHandler(pattern string, handler func(context.Context, *asynq.Task) error) { 55 | s.mux.HandleFunc(pattern, handler) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/transport/crontab/logger.go: -------------------------------------------------------------------------------- 1 | package crontab 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/go-eagle/eagle/pkg/log" 8 | ) 9 | 10 | type Logger struct { 11 | Log log.Logger 12 | } 13 | 14 | func (l Logger) Info(msg string, keysAndValues ...interface{}) { 15 | keysAndValues = formatTimes(keysAndValues) 16 | keysAndValues = append([]interface{}{ 17 | msg, 18 | }, keysAndValues...) 19 | l.Log.Infof(formatString(len(keysAndValues)), keysAndValues...) 20 | } 21 | 22 | func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) { 23 | keysAndValues = formatTimes(keysAndValues) 24 | keysAndValues = append([]interface{}{ 25 | msg, 26 | "error", err, 27 | }, keysAndValues...) 28 | l.Log.Errorf(formatString(len(keysAndValues)+2), keysAndValues...) 29 | } 30 | 31 | // formatString returns a logfmt-like format string for the number of 32 | // key/values. 33 | func formatString(numKeysAndValues int) string { 34 | var sb strings.Builder 35 | sb.WriteString("%s") 36 | if numKeysAndValues > 0 { 37 | sb.WriteString(", ") 38 | } 39 | for i := 0; i < numKeysAndValues/2; i++ { 40 | if i > 0 { 41 | sb.WriteString(", ") 42 | } 43 | sb.WriteString("%v=%v") 44 | } 45 | return sb.String() 46 | } 47 | 48 | // formatTimes formats any time.Time values as RFC3339. 49 | func formatTimes(keysAndValues []interface{}) []interface{} { 50 | var formattedArgs []interface{} 51 | for _, arg := range keysAndValues { 52 | if t, ok := arg.(time.Time); ok { 53 | arg = t.Format(time.RFC3339) 54 | } 55 | formattedArgs = append(formattedArgs, arg) 56 | } 57 | return formattedArgs 58 | } 59 | -------------------------------------------------------------------------------- /pkg/transport/grpc/codec.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | ) 6 | 7 | type codec struct{} 8 | 9 | func (c *codec) Marshal(v interface{}) ([]byte, error) { 10 | b, err := proto.Marshal(v.(proto.Message)) 11 | sentBytes.Add(float64(len(b))) 12 | return b, err 13 | } 14 | 15 | func (c *codec) Unmarshal(data []byte, v interface{}) error { 16 | receivedBytes.Add(float64(len(data))) 17 | return proto.Unmarshal(data, v.(proto.Message)) 18 | } 19 | 20 | func (c *codec) String() string { 21 | return "proto" 22 | } 23 | -------------------------------------------------------------------------------- /pkg/transport/grpc/interceptor.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/metadata" 8 | ) 9 | 10 | var ( 11 | MetadataClientAPIVersionKey = "client-api-version" 12 | ) 13 | 14 | // unaryServerInterceptor server unary interceptor 15 | func unaryServerInterceptor(s *Server) grpc.UnaryServerInterceptor { 16 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 17 | // preprocess stage 18 | md, ok := metadata.FromIncomingContext(ctx) 19 | if ok { 20 | ver, vs := "unknown", md.Get(MetadataClientAPIVersionKey) 21 | if len(vs) > 0 { 22 | ver = vs[0] 23 | } 24 | clientRequests.WithLabelValues("unary", ver).Inc() 25 | } 26 | 27 | return handler(ctx, req) 28 | } 29 | } 30 | 31 | // unaryClientInterceptor client unary interceptor 32 | func unaryClientInterceptor() grpc.UnaryClientInterceptor { 33 | return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 34 | // preprocess stage 35 | 36 | // call remote method 37 | err := invoker(ctx, method, req, reply, cc, opts...) 38 | 39 | // postprocess stage 40 | 41 | return err 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/transport/grpc/metrics.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | namespace = "grpc" 7 | 8 | sentBytes = prometheus.NewCounter(prometheus.CounterOpts{ 9 | Namespace: namespace, 10 | Subsystem: "network", 11 | Name: "client_grpc_sent_bytes_total", 12 | Help: "The total number of bytes sent to grpc clients.", 13 | }) 14 | 15 | receivedBytes = prometheus.NewCounter(prometheus.CounterOpts{ 16 | Namespace: namespace, 17 | Subsystem: "network", 18 | Name: "client_grpc_received_bytes_total", 19 | Help: "The total number of bytes received from grpc clients.", 20 | }) 21 | 22 | streamFailures = prometheus.NewCounterVec(prometheus.CounterOpts{ 23 | Namespace: namespace, 24 | Subsystem: "network", 25 | Name: "server_stream_failures_total", 26 | Help: "The total number of stream failures from the local server.", 27 | }, 28 | []string{"Type", "API"}, 29 | ) 30 | 31 | clientRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ 32 | Namespace: namespace, 33 | Subsystem: "server", 34 | Name: "client_requests_total", 35 | Help: "The total number of client requests per client version.", 36 | }, 37 | []string{"type", "client_api_version"}, 38 | ) 39 | ) 40 | 41 | func init() { 42 | prometheus.MustRegister(sentBytes) 43 | prometheus.MustRegister(receivedBytes) 44 | prometheus.MustRegister(streamFailures) 45 | prometheus.MustRegister(clientRequests) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/transport/grpc/resolver/direct/builder.go: -------------------------------------------------------------------------------- 1 | package direct 2 | 3 | import ( 4 | "strings" 5 | 6 | "google.golang.org/grpc/resolver" 7 | ) 8 | 9 | const name = "direct" 10 | 11 | func init() { 12 | resolver.Register(NewBuilder()) 13 | } 14 | 15 | type directBuilder struct{} 16 | 17 | // NewBuilder creates a directBuilder which is used to factory direct resolvers. 18 | // example: 19 | // direct:///127.0.0.1:9000,127.0.0.2:9000 20 | func NewBuilder() resolver.Builder { 21 | return &directBuilder{} 22 | } 23 | 24 | func (d *directBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { 25 | addrs := make([]resolver.Address, 0) 26 | for _, addr := range strings.Split(strings.TrimPrefix(target.URL.Path, "/"), ",") { 27 | addrs = append(addrs, resolver.Address{Addr: addr}) 28 | } 29 | err := cc.UpdateState(resolver.State{ 30 | Addresses: addrs, 31 | }) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return newDirectResolver(), nil 36 | } 37 | 38 | func (d *directBuilder) Scheme() string { 39 | return name 40 | } 41 | -------------------------------------------------------------------------------- /pkg/transport/grpc/resolver/direct/resolver.go: -------------------------------------------------------------------------------- 1 | package direct 2 | 3 | import "google.golang.org/grpc/resolver" 4 | 5 | type directResolver struct{} 6 | 7 | func newDirectResolver() resolver.Resolver { 8 | return &directResolver{} 9 | } 10 | 11 | func (r *directResolver) Close() { 12 | } 13 | 14 | func (r *directResolver) ResolveNow(options resolver.ResolveNowOptions) { 15 | } 16 | -------------------------------------------------------------------------------- /pkg/transport/grpc/server_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | -------------------------------------------------------------------------------- /pkg/transport/http/options.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-eagle/eagle/pkg/transport" 7 | ) 8 | 9 | var _ transport.Server = (*Server)(nil) 10 | 11 | // ServerOption is HTTP server option 12 | type ServerOption func(*Server) 13 | 14 | // WithAddress with server address. 15 | func WithAddress(addr string) ServerOption { 16 | return func(s *Server) { 17 | s.address = addr 18 | } 19 | } 20 | 21 | // WithReadTimeout with read timeout. 22 | func WithReadTimeout(timeout time.Duration) ServerOption { 23 | return func(s *Server) { 24 | s.readTimeout = timeout 25 | } 26 | } 27 | 28 | // WithWriteTimeout with write timeout. 29 | func WithWriteTimeout(timeout time.Duration) ServerOption { 30 | return func(s *Server) { 31 | s.writeTimeout = timeout 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/transport/http/server_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestServer(t *testing.T) { 10 | srv := NewServer() 11 | 12 | //go func() { 13 | if err := srv.Start(context.Background()); err != nil { 14 | panic(err) 15 | } 16 | //}() 17 | time.Sleep(time.Second) 18 | srv.Stop(context.Background()) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | ) 7 | 8 | // Server is transport server interface. 9 | type Server interface { 10 | Start(ctx context.Context) error 11 | Stop(ctx context.Context) error 12 | } 13 | 14 | // Endpoint is registry endpoint. 15 | type Endpoint interface { 16 | Endpoint() (*url.URL, error) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/utils/README.md: -------------------------------------------------------------------------------- 1 | # util 2 | 3 | 业务工具包 -------------------------------------------------------------------------------- /pkg/utils/debug.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // StackTrace return stack info 9 | func StackTrace(msg string, err interface{}) string { 10 | buf := make([]byte, 64*1024) 11 | buf = buf[:runtime.Stack(buf, false)] 12 | return fmt.Sprintf("%s, err: %s\nstack: %s", msg, err, buf) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/utils/debug_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestPrintStackTrace(t *testing.T) { 9 | t.Run("mock a error", func(t *testing.T) { 10 | err := StackTrace("mock a error", errors.New("throw a error")) 11 | t.Log(err) 12 | }) 13 | 14 | t.Run("mock a recover", func(t *testing.T) { 15 | defer func() { 16 | if r := recover(); r != nil { 17 | err := StackTrace("mock a recover", r) 18 | t.Log(err) 19 | } 20 | }() 21 | 22 | panic("throw a panic") 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/utils/ip_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /pkg/utils/time.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // DateTime date time layout 11 | DateTime = "2006-01-02 15:04:05" 12 | // DateOnly date layout 13 | DateOnly = "2006-01-02" 14 | // TimeOnly time layout 15 | TimeOnly = "15:04:05" 16 | ) 17 | 18 | // GetDate 获取字符串日期 19 | func GetDate() string { 20 | return time.Now().Format("2006/01/02") 21 | } 22 | 23 | // GetTodayDateInt 获取整形的日期 24 | func GetTodayDateInt() int { 25 | dateStr := time.Now().Format("200601") 26 | date, err := strconv.Atoi(dateStr) 27 | if err != nil { 28 | return 0 29 | } 30 | return date 31 | } 32 | 33 | // TimeLayout 常用日期格式化模板 34 | func TimeLayout() string { 35 | return DateTime 36 | } 37 | 38 | // TimeToString 时间转字符串 39 | func TimeToString(ts time.Time) string { 40 | return time.Unix(ts.Unix(), 00).Format(TimeLayout()) 41 | } 42 | 43 | // TimeToShortString 时间转日期 44 | func TimeToShortString(ts time.Time) string { 45 | return time.Unix(ts.Unix(), 00).Format("2006.01.02") 46 | } 47 | 48 | // GetShowTime 格式化时间 49 | func GetShowTime(ts time.Time) string { 50 | duration := time.Now().Unix() - ts.Unix() 51 | timeStr := "" 52 | if duration < 60 { 53 | timeStr = "刚刚发布" 54 | } else if duration < 3600 { 55 | timeStr = fmt.Sprintf("%d分钟前更新", duration/60) 56 | } else if duration < 86400 { 57 | timeStr = fmt.Sprintf("%d小时前更新", duration/3600) 58 | } else if duration < 86400*2 { 59 | timeStr = "昨天更新" 60 | } else { 61 | timeStr = TimeToShortString(ts) + "前更新" 62 | } 63 | return timeStr 64 | } 65 | -------------------------------------------------------------------------------- /pkg/utils/url.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/qiniu/api.v7/storage" 7 | ) 8 | 9 | // GetDefaultAvatarURL 获取默认头像 10 | func GetDefaultAvatarURL(cdnURL string) string { 11 | uri := "/default/avatar.jpg" 12 | return GetQiNiuPublicAccessURL(cdnURL, uri) 13 | } 14 | 15 | // GetAvatarURL user's avatar, if empty, use default avatar 16 | func GetAvatarURL(cdnURL, key string) string { 17 | if key == "" { 18 | return GetDefaultAvatarURL(cdnURL) 19 | } 20 | if strings.HasPrefix(key, "https://") { 21 | return key 22 | } 23 | return GetQiNiuPublicAccessURL(cdnURL, key) 24 | } 25 | 26 | // GetQiNiuPublicAccessURL 获取七牛资源的公有链接 27 | // 无需配置bucket, 域名会自动到域名所绑定的bucket去查找 28 | func GetQiNiuPublicAccessURL(cdnURL, path string) string { 29 | domain := cdnURL 30 | key := strings.TrimPrefix(path, "/") 31 | 32 | publicAccessURL := storage.MakePublicURL(domain, key) 33 | 34 | return publicAccessURL 35 | } 36 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGenShortID(t *testing.T) { 8 | shortID, err := GenShortID() 9 | if shortID == "" || err != nil { 10 | t.Error("GenShortID failed!") 11 | } 12 | 13 | t.Log("GenShortID test pass") 14 | } 15 | 16 | func BenchmarkGenShortID(b *testing.B) { 17 | for i := 0; i < b.N; i++ { 18 | GenShortID() 19 | } 20 | } 21 | 22 | func BenchmarkGenShortIDTimeConsuming(b *testing.B) { 23 | b.StopTimer() //调用该函数停止压力测试的时间计数 24 | 25 | shortID, err := GenShortID() 26 | if shortID == "" || err != nil { 27 | b.Error(err) 28 | } 29 | 30 | b.StartTimer() //重新开始时间 31 | 32 | for i := 0; i < b.N; i++ { 33 | GenShortID() 34 | } 35 | } 36 | 37 | func TestRandomStr(t *testing.T) { 38 | test := RandomStr(8) 39 | t.Log(test) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/utils/valid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // IsZero 检查是否是零值 8 | func IsZero(i ...interface{}) bool { 9 | ret := false 10 | for _, j := range i { 11 | v := reflect.ValueOf(j) 12 | if isZero(v) { 13 | return true 14 | } 15 | } 16 | return ret 17 | } 18 | 19 | func isZero(v reflect.Value) bool { 20 | switch v.Kind() { 21 | case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice: 22 | return v.IsNil() 23 | case reflect.Invalid: 24 | return true 25 | default: 26 | z := reflect.Zero(v.Type()) 27 | return reflect.DeepEqual(z.Interface(), v.Interface()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/version/base.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | gitTag = "" 5 | gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) 6 | gitTreeState = "not a git tree" // state of git tree, either "clean" or "dirty" 7 | buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/version/doc.go: -------------------------------------------------------------------------------- 1 | package version 2 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | // Info contains versioning information. 9 | type Info struct { 10 | GitTag string `json:"gitTag"` 11 | GitCommit string `json:"gitCommit"` 12 | GitTreeState string `json:"gitTreeState"` 13 | BuildDate string `json:"buildDate"` 14 | GoVersion string `json:"goVersion"` 15 | Compiler string `json:"compiler"` 16 | Platform string `json:"platform"` 17 | } 18 | 19 | // String returns info as a human-friendly version string. 20 | func (info *Info) String() string { 21 | return info.GitTag 22 | } 23 | 24 | // Get 返回详细的版本信息 25 | func Get() Info { 26 | return Info{ 27 | GitTag: gitTag, 28 | GitCommit: gitCommit, 29 | GitTreeState: gitTreeState, 30 | BuildDate: buildDate, 31 | GoVersion: runtime.Version(), 32 | Compiler: runtime.Compiler, 33 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # 脚本目录 2 | 3 | 存放用于执行各种构建,安装,分析等操作的脚本。 4 | Makefile 中执行的一些脚本可以放到这里,让 Makefile 变得更小巧、简单。 5 | 6 | 脚本文件 7 | - admin.sh # 进程的start|stop|status|restart控制文件 8 | - wrktest.sh # API 性能测试脚本 9 | 10 | ## Reference 11 | - 压测工具 https://github.com/tsenart/vegeta -------------------------------------------------------------------------------- /scripts/admin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERVER="eagle" 4 | BASE_DIR=$PWD 5 | INTERVAL=2 6 | 7 | # 命令行参数,需要手动指定 8 | ARGS="" 9 | 10 | function start() 11 | { 12 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 13 | echo "$SERVER already running" 14 | exit 1 15 | fi 16 | 17 | # set default 18 | if [ "$ENV" = "" ]; then 19 | ARGS="-c config/local" 20 | else 21 | ARGS="-c config/$ENV" 22 | fi 23 | 24 | nohup $BASE_DIR/$SERVER $ARGS server &>/dev/null & 25 | 26 | echo "starting..." && sleep $INTERVAL 27 | 28 | # check status 29 | if [ "`pgrep $SERVER -u $UID`" == "" ];then 30 | echo "$SERVER start failed" 31 | exit 1 32 | fi 33 | } 34 | 35 | function status() 36 | { 37 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 38 | echo $SERVER is running 39 | else 40 | echo $SERVER is not running 41 | fi 42 | } 43 | 44 | function stop() 45 | { 46 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 47 | kill -9 `pgrep $SERVER -u $UID` 48 | fi 49 | 50 | echo "stopping..." && sleep $INTERVAL 51 | 52 | if [ "`pgrep $SERVER -u $UID`" != "" ];then 53 | echo "$SERVER stop failed" 54 | exit 1 55 | fi 56 | } 57 | 58 | case "$1" in 59 | 'start') 60 | start 61 | ;; 62 | 'stop') 63 | stop 64 | ;; 65 | 'status') 66 | status 67 | ;; 68 | 'restart') 69 | stop && start 70 | ;; 71 | *) 72 | echo "usage: $0 {start|stop|restart|status}" 73 | exit 1 74 | ;; 75 | esac 76 | -------------------------------------------------------------------------------- /test/kafka-docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | zookeeper: 5 | image: wurstmeister/zookeeper:3.4.6 6 | expose: 7 | - "2181" 8 | restart: always 9 | container_name: zookeeper 10 | 11 | kafka: 12 | image: wurstmeister/kafka:2.12-2.3.0 13 | depends_on: 14 | - zookeeper 15 | ports: 16 | - "9093:9093" 17 | expose: 18 | - "9092" 19 | environment: 20 | KAFKA_CREATE_TOPICS: "go-message-broker-topic:2:1" 21 | KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9092,OUTSIDE://localhost:9093 22 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT 23 | KAFKA_LISTENERS: INSIDE://0.0.0.0:9092,OUTSIDE://0.0.0.0:9093 24 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 25 | KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE 26 | restart: always 27 | container_name: kafka -------------------------------------------------------------------------------- /test/nats-docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | nats: 5 | container_name: "nats" 6 | image: "nats" 7 | hostname: "nats" 8 | ports: 9 | - "4222:4222" 10 | - "6222:6222" 11 | - "8222:8222" 12 | labels: 13 | NAME: "nats" 14 | -------------------------------------------------------------------------------- /test/rabbitmq-docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | rabbitmq: 5 | container_name: "rabbitmq1" 6 | image: "rabbitmq:3-management" 7 | hostname: "rabbit" 8 | ports: 9 | - "15672:15672" 10 | - "5672:5672" 11 | labels: 12 | NAME: "rabbitmq" 13 | volumes: 14 | - ./rabbitmq-isolated.conf:/etc/rabbitmq/rabbitmq.config -------------------------------------------------------------------------------- /test/rabbitmq-isolated.conf: -------------------------------------------------------------------------------- 1 | [ 2 | {rabbit, 3 | [ 4 | %% The default "guest" user is only permitted to access the server 5 | %% via a loopback interface (e.g. localhost). 6 | %% {loopback_users, [<<"guest">>]}, 7 | %% 8 | %% Uncomment the following line if you want to allow access to the 9 | %% guest user from anywhere on the network. 10 | {loopback_users, []}, 11 | {default_vhost, "/"}, 12 | {default_user, "guest"}, 13 | {default_pass, "guest"}, 14 | {default_permissions, [".*", ".*", ".*"]} 15 | ]} 16 | ]. --------------------------------------------------------------------------------