├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ ├── feature-request.md │ ├── proposal.md │ └── question.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── app ├── cert.go ├── conf.go ├── conf │ ├── boot.pb.go │ ├── boot.proto │ └── boot.yml ├── control_plane.go ├── factory │ ├── lynx_default_factory.go │ └── plugin_factory.go ├── kratos │ └── kratos.go ├── log │ ├── banner.txt │ ├── conf │ │ ├── log.pb.go │ │ └── log.proto │ ├── logger.go │ └── lynx_log.go ├── lynx.go ├── plugin_manager.go ├── plugin_sort.go ├── runtime.go ├── subscribe │ ├── router.go │ ├── subscribe.go │ └── tls.go ├── tls │ ├── conf │ │ ├── tls.pb.go │ │ └── tls.proto │ └── tls.go └── util │ ├── bcrypt.go │ ├── bcrypt_test.go │ ├── jwt.go │ └── jwt_test.go ├── boot ├── conf.go └── strap.go ├── cmd └── lynx │ ├── go.mod │ ├── go.sum │ ├── internal │ ├── base │ │ ├── mod.go │ │ ├── path.go │ │ ├── repo.go │ │ └── vcs_url.go │ └── project │ │ ├── new.go │ │ └── project.go │ ├── main.go │ └── version.go ├── go.mod ├── go.sum ├── plugins ├── base.go ├── db │ ├── mysql │ │ ├── conf │ │ │ ├── mysql.pb.go │ │ │ └── mysql.proto │ │ ├── go.mod │ │ ├── go.sum │ │ ├── mysql.go │ │ └── plug.go │ └── pgsql │ │ ├── conf │ │ ├── pgsql.pb.go │ │ └── pgsql.proto │ │ ├── go.mod │ │ ├── go.sum │ │ ├── pgsql.go │ │ └── plug.go ├── deps.go ├── errors.go ├── events.go ├── health.go ├── id.go ├── mq │ └── kafka │ │ ├── conf │ │ ├── kafka.pb.go │ │ └── kafka.proto │ │ ├── errors.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── kafka.go │ │ ├── plug.go │ │ └── pool.go ├── nosql │ └── redis │ │ ├── conf │ │ ├── redis.pb.go │ │ └── redis.proto │ │ ├── go.mod │ │ ├── go.sum │ │ ├── plug.go │ │ ├── redis.go │ │ └── redislock │ │ └── lock.go ├── plugin.go ├── polaris │ ├── base.go │ ├── conf │ │ ├── polaris.pb.go │ │ └── polaris.proto │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── limiter.go │ ├── plug.go │ ├── polaris.go │ ├── registry.go │ └── router.go ├── seata │ ├── conf │ │ ├── seata.pb.go │ │ └── seata.proto │ ├── go.mod │ ├── go.sum │ ├── plug.go │ ├── seata.go │ └── seatago.yml ├── service │ ├── grpc │ │ ├── README.md │ │ ├── conf │ │ │ ├── grpc.pb.go │ │ │ └── grpc.proto │ │ ├── go.mod │ │ ├── go.sum │ │ ├── grpc.go │ │ ├── plug.go │ │ └── tls.go │ └── http │ │ ├── README.md │ │ ├── conf │ │ ├── http.pb.go │ │ └── http.proto │ │ ├── encoder.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── http.go │ │ ├── plug.go │ │ ├── tls.go │ │ └── tracer.go ├── tracer │ ├── conf │ │ ├── tracer.pb.go │ │ └── tracer.proto │ ├── go.mod │ ├── go.sum │ ├── plug.go │ └── tracer.go └── upg.go └── third_party ├── README.md ├── errors ├── errors.pb.go └── errors.proto ├── google ├── api │ ├── annotations.pb.go │ ├── annotations.proto │ ├── client.pb.go │ ├── client.proto │ ├── field_behavior.pb.go │ ├── field_behavior.proto │ ├── http.pb.go │ ├── http.proto │ ├── httpbody.pb.go │ └── httpbody.proto └── protobuf │ ├── any.pb.go │ ├── any.proto │ ├── api.pb.go │ ├── api.proto │ ├── compiler │ ├── plugin.pb.go │ └── plugin.proto │ ├── descriptor.pb.go │ ├── descriptor.proto │ ├── duration.pb.go │ ├── duration.proto │ ├── empty.pb.go │ ├── empty.proto │ ├── field_mask.pb.go │ ├── field_mask.proto │ ├── source_context.pb.go │ ├── source_context.proto │ ├── struct.pb.go │ ├── struct.proto │ ├── timestamp.pb.go │ ├── timestamp.proto │ ├── type.pb.go │ ├── type.proto │ ├── wrappers.pb.go │ └── wrappers.proto ├── openapi └── v3 │ ├── annotations.pb.go │ ├── annotations.proto │ ├── openapi.pb.go │ └── openapi.proto └── validate ├── README.md ├── validate.pb.go └── validate.proto /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: " 🐛 Bug Report" 3 | about: Report something that's broken. Please ensure your go-lynx version is still supported. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | #### What happened: 15 | 16 | #### What you expected to happen: 17 | 18 | #### How to reproduce it (as minimally and precisely as possible): 19 | 20 | #### Anything else we need to know?: 21 | 22 | #### Environment: 23 | 24 | - Lynx version (use `lynx -v`): 25 | - Go version (use `go version`): 26 | - OS (e.g: `cat /etc/os-release`): 27 | - Others: 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "💡 Feature Request" 3 | about: For ideas or feature requests, start a new discussion. 4 | title: "[Feature]" 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | Please see the FAQ in our main README.md before submitting your issue. 10 | 11 | 36 | 37 | ### What problem is the feature used to solve? 38 | 39 | 43 | 44 | ### Requirements description of the feature 45 | 46 | 50 | 51 | ### References 52 | 53 | 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📝 Proposal Request" 3 | about: Implementation draft of feature. 4 | title: "[Proposal]" 5 | labels: proposal 6 | assignees: '' 7 | --- 8 | 9 | Please see the FAQ in our main README.md before submitting your issue. 10 | 11 | 36 | 37 | ### Proposal description 38 | 39 | 43 | 44 | ### Implementation mode 45 | 46 | 66 | 67 | ### Usage demonstration 68 | 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Question" 3 | about: Ask a question about go-lynx. 4 | title: "[Question]" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please see the FAQ in our main README.md before submitting your issue. 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Reference https://github.com/github/gitignore/blob/master/Go.gitignore 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 21 | *.o 22 | *.a 23 | *.so 24 | 25 | # OS General 26 | Thumbs.db 27 | .DS_Store 28 | 29 | # project 30 | *.cert 31 | *.key 32 | *.log 33 | bin/ 34 | 35 | # Develop tools 36 | .vscode/ 37 | .idea/ 38 | *.swp 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOHOSTOS:=$(shell go env GOHOSTOS) 2 | GOPATH:=$(shell go env GOPATH) 3 | VERSION=$(shell git describe --tags --always) 4 | 5 | ifeq ($(GOHOSTOS), windows) 6 | #the `find.exe` is different from `find` in bash/shell. 7 | #to see https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/find. 8 | #changed to use git-bash.exe to run find cli or other cli friendly, caused of every developer has a Git. 9 | #Git_Bash= $(subst cmd\,bin\bash.exe,$(dir $(shell where git))) 10 | Git_Bash=$(subst \,/,$(subst cmd\,bin\bash.exe,$(dir $(shell where git)))) 11 | CONF_PROTO_FILES=$(shell $(Git_Bash) -c "find conf -name *.proto") 12 | else 13 | CONF_PROTO_FILES=$(shell find conf -name *.proto) 14 | endif 15 | 16 | .PHONY: init 17 | # init env 18 | init: 19 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 20 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 21 | go install github.com/go-kratos/kratos/cmd/kratos/v2@latest 22 | go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest 23 | go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest 24 | go install github.com/google/wire/cmd/wire@latest 25 | 26 | 27 | .PHONY: config 28 | # generate config 29 | config: 30 | for PROTO_FILE in $$(find . -name '*.proto'); do \ 31 | DIR=$$(dirname "$$PROTO_FILE"); \ 32 | protoc --proto_path="$$DIR" -I ./third_party -I ./boot -I ./app --go_out=paths=source_relative:"$$DIR" "$$PROTO_FILE"; \ 33 | done 34 | 35 | MODULES_VERSION ?= 36 | MODULES ?= 37 | 38 | .PHONY: tag 39 | # tag modules with version 40 | tag: 41 | @if [ -z "$(MODULES_VERSION)" ]; then \ 42 | echo "❌ MODULES_VERSION is required. Usage: make tag MODULES_VERSION=v2.0.0 MODULES=\"plugins/xxx plugins/yyy\""; \ 43 | exit 1; \ 44 | fi 45 | @if [ -z "$(MODULES)" ]; then \ 46 | echo "❌ MODULES is required. Usage: make tag MODULES_VERSION=v2.0.0 MODULES=\"plugins/xxx plugins/yyy\""; \ 47 | exit 1; \ 48 | fi 49 | @echo "Tagging modules with version $(MODULES_VERSION)..." 50 | @for module in $(MODULES); do \ 51 | TAG="$$module/$(MODULES_VERSION)"; \ 52 | echo "Creating tag $$TAG"; \ 53 | git tag $$TAG || { echo "Failed to tag $$TAG"; exit 1; }; \ 54 | done 55 | 56 | .PHONY: push-tags 57 | # push tags to origin 58 | push-tags: 59 | @if [ -z "$(MODULES_VERSION)" ]; then \ 60 | echo "❌ MODULES_VERSION is required. Usage: make push-tags MODULES_VERSION=v2.0.0 MODULES=\"plugins/xxx plugins/yyy\""; \ 61 | exit 1; \ 62 | fi 63 | @if [ -z "$(MODULES)" ]; then \ 64 | echo "❌ MODULES is required. Usage: make push-tags MODULES_VERSION=v2.0.0 MODULES=\"plugins/xxx plugins/yyy\""; \ 65 | exit 1; \ 66 | fi 67 | @echo "Pushing tags to origin..." 68 | @for module in $(MODULES); do \ 69 | TAG="$$module/$(MODULES_VERSION)"; \ 70 | echo "Pushing tag $$TAG"; \ 71 | git push origin $$TAG || { echo "Failed to push $$TAG"; exit 1; }; \ 72 | done 73 | 74 | .PHONY: release 75 | # release modules with version 76 | release: tag push-tags 77 | 78 | # show help 79 | help: 80 | @echo '' 81 | @echo 'Usage:' 82 | @echo ' make [target]' 83 | @echo '' 84 | @echo 'Targets:' 85 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 86 | helpMessage = match(lastLine, /^# (.*)/); \ 87 | if (helpMessage) { \ 88 | helpCommand = substr($$1, 0, index($$1, ":")); \ 89 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \ 90 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \ 91 | } \ 92 | } \ 93 | { lastLine = $$0 }' $(MAKEFILE_LIST) 94 | 95 | .DEFAULT_GOAL := help 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | 11 | ##### Translate to: [简体中文](README_zh.md) 12 | 13 | ## Lynx: The Plug-and-Play Go Microservices Framework 14 | 15 | > Lynx is a revolutionary open-source microservices framework, offering a seamless plug-and-play experience for 16 | > developers. Built on the robust foundations of Seata , Polaris and Kratos, Lynx's primary objective is to simplify the 17 | > microservices development process. It allows developers to focus their efforts on crafting business logic, rather than 18 | > getting entangled in the complexities of microservice infrastructure. 19 | 20 | ## Key Features 21 | 22 | > Lynx is equipped with a comprehensive set of key microservices capabilities, including: 23 | 24 | - **Service Registration and Discovery:** Simplifies the process of microservice registration and discovery. 25 | - **Encrypted Intranet Communication:** Ensures data security within the microservice architecture, guaranteeing trust 26 | and reliability. 27 | - **Rate Limiting:** Prevents microservice overload, ensuring robustness of the microservices and a high-quality user 28 | experience. 29 | - **Routing:** Intelligent routing to specified version of microservices, providing multi-version deployment, 30 | blue-green, and canary release capabilities. 31 | - **Fallback:** Provides graceful fault handling, ensuring service availability and resilience. 32 | - **Distributed Transactions:** Simplifies transaction management across multiple services, promoting data consistency 33 | and reliability. 34 | 35 | ## Plugin-Driven Modular Design 36 | 37 | > Lynx proudly introduces a plugin-driven modular design, enabling the combination of microservice functionality modules 38 | > through plugins. This unique approach allows for high customizability and adaptability to diverse business needs. Any 39 | > third-party tool can be effortlessly integrated as a plugin, providing a flexible and extensible platform for 40 | > developers. Lynx is committed to simplifying the microservices ecosystem, delivering an efficient and user-friendly 41 | > platform for developers. 42 | 43 | > In future versions, Lynx will develop and integrate more middleware, improving microservice scalability while 44 | > incorporating more mainstream framework components. 45 | 46 | ## Built With 47 | 48 | Lynx harnesses the power of several open-source projects for its core components, including: 49 | 50 | - [Seata](https://github.com/seata/seata) 51 | - [Kratos](https://github.com/go-kratos/kratos) 52 | - [Polaris](https://github.com/polarismesh/polaris) 53 | 54 | ## Quick Install 55 | 56 | > If you want to use this lynx microservice, all you need to do is execute the following command to install the Lynx CLI 57 | > command-line tool, and then run the new command to automatically initialize a runnable project (the new command can 58 | > support multiple project names). 59 | 60 | ```shell 61 | go install github.com/go-lynx/lynx/cmd/lynx@latest 62 | ``` 63 | 64 | ```shell 65 | lynx new demo1 demo2 demo3 66 | ``` 67 | 68 | ## Quick Start Code 69 | 70 | To get your microservice up and running in no time, use the following code (Some functionalities can be plugged in or 71 | out based on your configuration file.): 72 | 73 | ```go 74 | func main() { 75 | boot.LynxApplication(wireApp).Run() 76 | } 77 | ``` 78 | 79 | Join us in our journey to simplify microservices development with Lynx, the plug-and-play Go Microservices Framework. 80 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | Translations: [English](README.md) | [简体中文](README_zh.md) 12 | 13 | ## Lynx:即插即用的 Go 微服务框架 14 | 15 | > Lynx 是一款革命性的开源微服务框架,为开发者提供无缝的即插即用体验。Lynx 建立在 Seata, Polaris 和 Kratos 16 | > 的坚实基础之上,其主要目标是简化微服务的开发过程。它让开发者可以将精力集中在编写业务逻辑上,而不是陷入微服务基础设施的复杂性中。 17 | 18 | ## 主要特性 19 | 20 | > Lynx 配备了一套综合的微服务关键能力,包括: 21 | 22 | - **服务注册与发现:** 简化了微服务注册和发现服务的过程。 23 | - **加密的内网通信:** 保障了微服务架构中通信数据安全,确保了服务之间的信任和数据可靠性。 24 | - **限流:** 防止微服务过载,确保微服务健壮性和高质量的用户体验。 25 | - **路由:** 智能路由指定微服务版本,提供了多版本流量路由,蓝绿,灰度发布能力。 26 | - **降级:** 提供了优雅的故障处理,确保服务的可用性和弹性。 27 | - **分布式事务:** 简化了跨多个服务的事务管理,促进了数据的一致性和可靠性。 28 | 29 | ## 插件驱动的模块化设计 30 | 31 | > Lynx 自豪地推出了插件驱动的模块化设计,通过插件实现微服务功能模块的组合。这种独特的方法允许高度定制化和适应多样化的业务需求。任何第三方工具都可以轻松地作为插件集成,为开发者提供一个灵活和可扩展的平台。Lynx 32 | > 致力于简化微服务生态系统,为开发者提供一个高效和用户友好的平台。 33 | 34 | > 在未来的版本中 Lynx 将会开发与集成更多的中间件,提高微服务可扩展性的同时纳入更多主流框架组件。 35 | 36 | ## 构建所用 37 | 38 | Lynx 利用了几个开源项目的力量作为其核心组件,包括: 39 | 40 | - [Seata](https://github.com/seata/seata) 41 | - [Kratos](https://github.com/go-kratos/kratos) 42 | - [Polaris](https://github.com/polarismesh/polaris) 43 | 44 | ## 快速安装 45 | 46 | > 如果你想使用这个 Lynx 微服务,你只需要执行以下命令安装 Lynx CLI 命令行工具,然后运行 new 命令自动初始化一个可运行的项目(new 47 | > 命令可以支持多个项目名称)。 48 | 49 | ```shell 50 | go install github.com/go-lynx/lynx/cmd/lynx@latest 51 | ``` 52 | 53 | ```shell 54 | lynx new demo1 demo2 demo3 55 | ``` 56 | 57 | ## 快速开始代码 58 | 59 | 想要快速启动你的微服务,使用以下代码(一些功能可以根据你的配置文件插入或移出): 60 | 61 | ```go 62 | func main() { 63 | boot.LynxApplication(wireApp).Run() 64 | } 65 | ``` 66 | 67 | 来和我们一起,使用 Lynx,即插即用的 Go 微服务框架,简化微服务开发,期待你的加入。 68 | -------------------------------------------------------------------------------- /app/cert.go: -------------------------------------------------------------------------------- 1 | // Package app provides core functionality for the Lynx application framework. 2 | package app 3 | 4 | // CertificateProvider defines an interface for managing TLS/SSL certificates. 5 | // It provides methods to access the certificate, private key, and root Certificate Authority (CA) 6 | // certificate required for secure communication. 7 | // 8 | // CertificateProvider 定义了管理 TLS/SSL 证书的接口。 9 | // 它提供了访问证书、私钥和根证书颁发机构(CA)证书的方法, 10 | // 这些都是安全通信所必需的。 11 | type CertificateProvider interface { 12 | // GetCertificate returns the TLS/SSL certificate as a byte slice. 13 | // The certificate should be in PEM format. 14 | // GetCertificate 返回 PEM 格式的 TLS/SSL 证书字节切片。 15 | GetCertificate() []byte 16 | 17 | // GetPrivateKey returns the private key associated with the certificate as a byte slice. 18 | // The private key should be in PEM format. 19 | // GetPrivateKey 返回 PEM 格式的与证书关联的私钥字节切片。 20 | GetPrivateKey() []byte 21 | 22 | // GetRootCACertificate returns the root CA certificate as a byte slice. 23 | // The root CA certificate should be in PEM format. 24 | // This certificate is used to verify the trust chain. 25 | // GetRootCACertificate 返回 PEM 格式的根 CA 证书字节切片。 26 | // 此证书用于验证信任链。 27 | GetRootCACertificate() []byte 28 | } 29 | 30 | // Certificate returns the current application's certificate provider. 31 | // Returns nil if no certificate provider has been set. 32 | // 33 | // Certificate 返回当前应用的证书提供者。 34 | // 如果未设置证书提供者,则返回 nil。 35 | func (a *LynxApp) Certificate() CertificateProvider { 36 | return a.cert 37 | } 38 | 39 | // SetCertificateProvider configures the certificate provider for the application. 40 | // The provider parameter must implement the CertificateProvider interface. 41 | // 42 | // SetCertificateProvider 配置应用的证书提供者。 43 | // provider 参数必须实现 CertificateProvider 接口。 44 | func (a *LynxApp) SetCertificateProvider(provider CertificateProvider) { 45 | a.cert = provider 46 | } 47 | -------------------------------------------------------------------------------- /app/conf.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-lynx/lynx/app/log" 6 | 7 | "github.com/go-kratos/kratos/v2/config" 8 | ) 9 | 10 | // PreparePlug 方法通过远程或本地配置文件引导插件加载。 11 | // 它基于配置处理插件的初始化和注册操作。 12 | // 返回一个成功准备好的插件名称列表。 13 | func (m *DefaultLynxPluginManager) PreparePlug(config config.Config) []string { 14 | // 检查配置是否为 nil,如果为 nil 则记录错误日志并返回 nil 15 | if config == nil { 16 | log.Error("Configuration is nil") 17 | return nil 18 | } 19 | 20 | // 获取包含所有已注册插件配置前缀的注册表 21 | table := m.factory.GetPluginRegistry() 22 | // 检查注册表是否为空,如果为空则记录警告日志并返回 nil 23 | if len(table) == 0 { 24 | log.Warn("No plugins registered in factory") 25 | return nil 26 | } 27 | 28 | // 初始化一个切片,用于存储待加载的插件名称,预分配容量为注册表的长度 29 | plugNames := make([]string, 0, len(table)) 30 | 31 | // 遍历配置前缀 32 | for confPrefix, names := range table { 33 | // 检查配置前缀是否为空,如果为空则记录警告日志并跳过当前循环 34 | if confPrefix == "" { 35 | log.Warnf("Empty configuration prefix found, skipping") 36 | continue 37 | } 38 | 39 | // 尝试获取当前前缀对应的配置值 40 | cfg := config.Value(confPrefix) 41 | // 检查配置值是否为 nil,如果为 nil 则记录调试日志并跳过当前循环 42 | if cfg == nil { 43 | log.Debugf("No configuration found for prefix: %s", confPrefix) 44 | continue 45 | } 46 | 47 | // 加载配置值,如果加载结果为 nil 则记录调试日志并跳过当前循环 48 | if loaded := cfg.Load(); loaded == nil { 49 | log.Debugf("Configuration cfg is nil for prefix: %s", confPrefix) 50 | continue 51 | } 52 | 53 | // 检查是否有与前缀关联的插件名称,如果没有则记录调试日志并跳过当前循环 54 | if len(names) == 0 { 55 | log.Debugf("No plugins associated with prefix: %s", confPrefix) 56 | continue 57 | } 58 | 59 | // 处理每个插件名称 60 | for _, name := range names { 61 | // 检查插件名称是否为空,如果为空则记录警告日志并跳过当前循环 62 | if name == "" { 63 | log.Warn("Empty plugin name found, skipping") 64 | continue 65 | } 66 | 67 | // 检查插件是否已存在且能否创建 68 | if err := m.preparePlugin(name); err != nil { 69 | continue 70 | } 71 | 72 | // 将成功准备的插件名称添加到切片中 73 | plugNames = append(plugNames, name) 74 | } 75 | } 76 | 77 | // 检查是否有成功准备的插件,如果没有则记录警告日志,否则记录成功信息 78 | if len(plugNames) != 0 { 79 | log.Infof("successfully prepared %d plugins", len(plugNames)) 80 | } 81 | 82 | return plugNames 83 | } 84 | 85 | // preparePlugin 处理单个插件的准备工作。 86 | // 它会检查插件是否已存在,创建插件实例,并将其添加到管理器中。 87 | // 如果任何步骤失败,则返回错误。 88 | func (m *DefaultLynxPluginManager) preparePlugin(name string) error { 89 | // 检查插件是否已经加载,如果已加载则返回错误信息 90 | if _, exists := m.pluginMap.Load(name); exists { 91 | return fmt.Errorf("plugin %s is already loaded", name) 92 | } 93 | 94 | // 验证插件是否存在于工厂中,如果不存在则返回错误信息 95 | if !m.factory.HasPlugin(name) { 96 | return fmt.Errorf("plugin %s does not exist in factory", name) 97 | } 98 | 99 | // 创建插件实例,如果创建失败则返回错误信息 100 | p, err := m.factory.CreatePlugin(name) 101 | if err != nil { 102 | return fmt.Errorf("failed to create plugin %s: %v", name, err) 103 | } 104 | 105 | // 检查创建的插件实例是否为 nil,如果为 nil 则返回错误信息 106 | if p == nil { 107 | return fmt.Errorf("created plugin %s is nil", name) 108 | } 109 | 110 | // 将插件添加到管理器的跟踪结构中 111 | m.mu.Lock() 112 | m.pluginList = append(m.pluginList, p) 113 | m.mu.Unlock() 114 | m.pluginMap.Store(p.Name(), p) 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /app/conf/boot.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.app.conf; 4 | 5 | option go_package = "github.com/go-lynx/lynx/conf"; 6 | 7 | // Bootstrap 消息表示应用程序的启动配置信息。 8 | message Bootstrap { 9 | // lynx 字段包含 Lynx 框架相关的配置信息。 10 | Lynx lynx = 1; 11 | } 12 | 13 | // Lynx 消息封装了 Lynx 框架的应用程序配置。 14 | message Lynx { 15 | // application 字段包含应用程序自身的配置信息。 16 | Application application = 1; 17 | } 18 | 19 | // Application 消息包含应用程序的基本配置信息。 20 | message Application { 21 | // name 字段表示应用程序的名称。 22 | string name = 1; 23 | // version 字段表示应用程序的版本号。 24 | string version = 2; 25 | // close_banner 字段用于控制是否关闭应用程序启动时显示的横幅信息。 26 | bool close_banner = 3; 27 | } 28 | -------------------------------------------------------------------------------- /app/conf/boot.yml: -------------------------------------------------------------------------------- 1 | # 根配置项,代表 Lynx 应用的全局配置 2 | lynx: 3 | # 应用程序相关配置 4 | application: 5 | # 应用程序的名称,用于标识该应用 6 | name: name 7 | # 应用程序的版本号,遵循语义化版本规范 8 | version: v1.0.0 9 | # Polaris 服务治理相关配置 10 | polaris: 11 | # Polaris 命名空间,用于隔离不同环境或业务的资源 12 | namespace: dev 13 | # 访问 Polaris 服务的认证令牌 14 | token: polaris-token 15 | # 服务实例的权重,用于负载均衡,数值越大被选中的概率越高 16 | weight: 100 17 | # 服务实例的存活时间(TTL),单位为秒,用于心跳检测 18 | ttl: 5 19 | # 请求 Polaris 服务的超时时间,支持时间单位后缀,如 s(秒)、ms(毫秒)等 20 | timeout: 5s 21 | -------------------------------------------------------------------------------- /app/factory/lynx_default_factory.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "github.com/go-lynx/lynx/plugins" 7 | ) 8 | 9 | // Global factory instance 10 | // 全局插件工厂实例,用于实现单例模式 11 | var ( 12 | globalPluginFactory *LynxPluginFactory 13 | once sync.Once 14 | ) 15 | 16 | // GlobalPluginFactory returns the singleton instance of the plugin factory. 17 | // GlobalPluginFactory 返回插件工厂的单例实例。 18 | func GlobalPluginFactory() PluginFactory { 19 | once.Do(func() { 20 | globalPluginFactory = newDefaultPluginFactory() 21 | }) 22 | return globalPluginFactory 23 | } 24 | 25 | // LynxPluginFactory implements the PluginFactory interface. 26 | // LynxPluginFactory 实现了 PluginFactory 接口。 27 | type LynxPluginFactory struct { 28 | // configToPlugins maps configuration prefixes to their associated plugin names. 29 | // Example: "http" -> ["http_server", "http_client"] 30 | // configToPlugins 将配置前缀映射到关联的插件名称。 31 | // 示例: "http" -> ["http_server", "http_client"] 32 | configToPlugins map[string][]string 33 | 34 | // pluginCreators stores the creation functions for each plugin. 35 | // Maps plugin names to their respective creation functions. 36 | // pluginCreators 存储每个插件的创建函数。 37 | // 将插件名称映射到各自的创建函数。 38 | pluginCreators map[string]func() plugins.Plugin 39 | } 40 | 41 | // newDefaultPluginFactory initializes a new instance of LynxPluginFactory. 42 | // newDefaultPluginFactory 初始化一个新的 LynxPluginFactory 实例。 43 | func newDefaultPluginFactory() *LynxPluginFactory { 44 | return &LynxPluginFactory{ 45 | configToPlugins: make(map[string][]string), 46 | pluginCreators: make(map[string]func() plugins.Plugin), 47 | } 48 | } 49 | 50 | // RegisterPlugin registers a new plugin with its configuration prefix and creation function. 51 | // Panics if a plugin with the same name is already registered. 52 | // RegisterPlugin 使用插件的配置前缀和创建函数注册一个新插件。 53 | // 如果同名插件已注册,则触发 panic。 54 | func (f *LynxPluginFactory) RegisterPlugin(name string, configPrefix string, creator func() plugins.Plugin) { 55 | if _, exists := f.pluginCreators[name]; exists { 56 | panic(fmt.Errorf("plugin already registered: %s", name)) 57 | } 58 | 59 | f.pluginCreators[name] = creator 60 | 61 | pluginList := f.configToPlugins[configPrefix] 62 | if pluginList == nil { 63 | f.configToPlugins[configPrefix] = []string{name} 64 | } else { 65 | f.configToPlugins[configPrefix] = append(pluginList, name) 66 | } 67 | } 68 | 69 | // UnregisterPlugin removes a plugin from both the creator map and configuration mapping. 70 | // UnregisterPlugin 从创建函数映射和配置映射中移除一个插件。 71 | func (f *LynxPluginFactory) UnregisterPlugin(name string) { 72 | // Remove from creator map 73 | // 从创建函数映射中移除插件 74 | delete(f.pluginCreators, name) 75 | 76 | // Remove from configuration mapping 77 | // 从配置映射中移除插件 78 | for prefix, pluginList := range f.configToPlugins { 79 | for i, plugin := range pluginList { 80 | if plugin == name { 81 | // Remove the plugin from the slice 82 | // 从切片中移除插件 83 | f.configToPlugins[prefix] = append(pluginList[:i], pluginList[i+1:]...) 84 | 85 | // If no pluginList left for this prefix, remove the prefix entry 86 | // 如果该前缀下没有剩余插件,则移除该前缀条目 87 | if len(f.configToPlugins[prefix]) == 0 { 88 | delete(f.configToPlugins, prefix) 89 | } 90 | break 91 | } 92 | } 93 | } 94 | } 95 | 96 | // GetPluginRegistry returns the current mapping of configuration prefixes to plugin names. 97 | // GetPluginRegistry 返回当前配置前缀到插件名称的映射。 98 | func (f *LynxPluginFactory) GetPluginRegistry() map[string][]string { 99 | return f.configToPlugins 100 | } 101 | 102 | // CreatePlugin creates a new instance of a plugin by its name. 103 | // Returns an error if the plugin is not registered. 104 | // CreatePlugin 根据插件名称创建一个新的插件实例。 105 | // 如果插件未注册,则返回错误。 106 | func (f *LynxPluginFactory) CreatePlugin(name string) (plugins.Plugin, error) { 107 | creator, exists := f.pluginCreators[name] 108 | if !exists { 109 | return nil, fmt.Errorf("plugin not found: %s", name) 110 | } 111 | return creator(), nil 112 | } 113 | 114 | // HasPlugin checks if a plugin is registered in the factory. 115 | // HasPlugin 检查插件是否在工厂中注册。 116 | func (f *LynxPluginFactory) HasPlugin(name string) bool { 117 | _, exists := f.pluginCreators[name] 118 | return exists 119 | } 120 | -------------------------------------------------------------------------------- /app/factory/plugin_factory.go: -------------------------------------------------------------------------------- 1 | // Package factory provides functionality for creating and managing plugins in the Lynx framework. 2 | package factory 3 | 4 | import ( 5 | "github.com/go-lynx/lynx/plugins" 6 | ) 7 | 8 | // PluginFactory defines the complete interface for plugin management, 9 | // combining both creation and registry capabilities. 10 | // PluginFactory 定义了插件管理的完整接口,结合了插件创建和注册功能。 11 | type PluginFactory interface { 12 | PluginCreator 13 | PluginRegistry 14 | } 15 | 16 | // PluginCreator defines the interface for creating plugin instances. 17 | // PluginCreator 定义了创建插件实例的接口。 18 | type PluginCreator interface { 19 | // CreatePlugin instantiates a new plugin instance by its name. 20 | // Returns an error if the plugin cannot be created. 21 | // CreatePlugin 根据插件名称实例化一个新的插件实例。 22 | // 如果插件无法创建,则返回错误。 23 | CreatePlugin(name string) (plugins.Plugin, error) 24 | } 25 | 26 | // PluginRegistry defines the interface for managing plugin registrations. 27 | // PluginRegistry 定义了管理插件注册的接口。 28 | type PluginRegistry interface { 29 | // RegisterPlugin adds a new plugin to the registry with its configuration prefix 30 | // and creation function. 31 | // RegisterPlugin 使用插件的配置前缀和创建函数将新插件添加到注册表中。 32 | RegisterPlugin(name string, configPrefix string, creator func() plugins.Plugin) 33 | 34 | // UnregisterPlugin removes a plugin from the registry. 35 | // UnregisterPlugin 从注册表中移除一个插件。 36 | UnregisterPlugin(name string) 37 | 38 | // GetPluginRegistry returns the mapping of configuration prefixes to plugin names. 39 | // GetPluginRegistry 返回配置前缀到插件名称的映射。 40 | GetPluginRegistry() map[string][]string 41 | 42 | // HasPlugin checks if a plugin is registered with the given name. 43 | // HasPlugin 检查具有给定名称的插件是否已注册。 44 | HasPlugin(name string) bool 45 | } 46 | -------------------------------------------------------------------------------- /app/kratos/kratos.go: -------------------------------------------------------------------------------- 1 | // Package kratos provides integration with the Kratos framework 2 | package kratos 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/go-kratos/kratos/v2/transport" 8 | "github.com/go-lynx/lynx/app/log" 9 | 10 | "github.com/go-kratos/kratos/v2" 11 | "github.com/go-kratos/kratos/v2/registry" 12 | "github.com/go-kratos/kratos/v2/transport/grpc" 13 | "github.com/go-kratos/kratos/v2/transport/http" 14 | "github.com/go-lynx/lynx/app" 15 | ) 16 | 17 | // Options holds the configuration for creating a Kratos application 18 | // Options 结构体用于存储创建 Kratos 应用所需的配置信息 19 | type Options struct { 20 | // GRPCServer instance 21 | // gRPC 服务器实例 22 | GRPCServer *grpc.Server 23 | // HTTPServer instance 24 | // HTTP 服务器实例 25 | HTTPServer *http.Server 26 | // Registrar for service registration 27 | // 用于服务注册的注册器 28 | Registrar registry.Registrar 29 | } 30 | 31 | // NewKratos creates a new Kratos application with the specified options. 32 | // It supports creating applications with HTTP server, gRPC server, or both. 33 | // 34 | // Parameters: 35 | // - opts: Options struct containing all necessary configuration 36 | // 37 | // Returns: 38 | // - *kratos.App: The created Kratos application 39 | // - error: Any error that occurred during creation 40 | // 41 | // NewKratos 使用指定的选项创建一个新的 Kratos 应用程序。 42 | // 支持创建包含 HTTP 服务器、gRPC 服务器或两者兼有的应用程序。 43 | // 44 | // 参数: 45 | // - opts: 包含所有必要配置的 Options 结构体 46 | // 47 | // 返回值: 48 | // - *kratos.App: 创建好的 Kratos 应用程序实例 49 | // - error: 创建过程中发生的任何错误 50 | func NewKratos(opts Options) (*kratos.App, error) { 51 | // Validate required fields 52 | if app.GetHost() == "" { 53 | return nil, fmt.Errorf("host cannot be empty") 54 | } 55 | if app.GetName() == "" { 56 | return nil, fmt.Errorf("service name cannot be empty") 57 | } 58 | if log.Logger == nil { 59 | return nil, fmt.Errorf("logger cannot be nil") 60 | } 61 | 62 | // Prepare base options for Kratos application 63 | kratosOpts := []kratos.Option{ 64 | // Set the application ID to the host name 65 | kratos.ID(app.GetHost()), 66 | // Set the application name 67 | kratos.Name(app.GetName()), 68 | // Set the application version 69 | kratos.Version(app.GetVersion()), 70 | // Set the application metadata with basic info 71 | kratos.Metadata(map[string]string{ 72 | "host": app.GetHost(), 73 | "version": app.GetVersion(), 74 | }), 75 | // Set the application logger 76 | kratos.Logger(log.Logger), 77 | // Set the application registrar 78 | kratos.Registrar(opts.Registrar), 79 | } 80 | 81 | // Collect all available transport servers based on the provided options. 82 | // 根据提供的选项收集所有可用的传输服务器。 83 | // This function checks for the presence of both gRPC and HTTP servers 84 | // in the options and adds them to a list of transport servers. 85 | // 此函数会检查选项中是否存在 gRPC 和 HTTP 服务器, 86 | // 并将它们添加到传输服务器列表中。 87 | // If any servers are available, it appends them to the Kratos application options. 88 | // 如果有可用的服务器,会将它们添加到 Kratos 应用程序选项中。 89 | // 90 | // Variables: 91 | // - serverList: A slice to hold all available transport servers. 92 | // 变量: 93 | // - serverList: 用于存储所有可用传输服务器的切片。 94 | var serverList []transport.Server 95 | 96 | // Check if a gRPC server instance is provided in the options. 97 | // 检查选项中是否提供了 gRPC 服务器实例。 98 | // If available, add it to the list of transport servers. 99 | // 如果可用,将其添加到传输服务器列表中。 100 | if opts.GRPCServer != nil { 101 | // Add the gRPC server to the list of transport servers 102 | // 将 gRPC 服务器添加到传输服务器列表中 103 | serverList = append(serverList, opts.GRPCServer) 104 | } 105 | 106 | // Check if an HTTP server instance is provided in the options. 107 | // 检查选项中是否提供了 HTTP 服务器实例。 108 | // If available, add it to the list of transport servers. 109 | // 如果可用,将其添加到传输服务器列表中。 110 | if opts.HTTPServer != nil { 111 | // Add the HTTP server to the list of transport servers 112 | // 将 HTTP 服务器添加到传输服务器列表中 113 | serverList = append(serverList, opts.HTTPServer) 114 | } 115 | 116 | // Check if there are any servers in the list. 117 | // 检查列表中是否有服务器。 118 | // If so, append them to the Kratos application options. 119 | // 如果有,将它们添加到 Kratos 应用程序选项中。 120 | if len(serverList) > 0 { 121 | // Add all collected servers to the Kratos application options 122 | // 将所有收集到的服务器添加到 Kratos 应用程序选项中 123 | kratosOpts = append(kratosOpts, kratos.Server(serverList...)) 124 | } 125 | 126 | // Create and return the Kratos application 127 | // 创建并返回 Kratos 应用程序 128 | return kratos.New(kratosOpts...), nil 129 | } 130 | 131 | // ProvideKratosOptions 根据传入的参数创建并返回一个 Options 结构体实例。 132 | // 该结构体用于存储创建 Kratos 应用所需的配置信息。 133 | // 134 | // 参数: 135 | // - logger: 应用程序使用的日志记录器。 136 | // - grpcServer: gRPC 服务器实例。 137 | // - httpServer: HTTP 服务器实例。 138 | // - registrar: 用于服务注册的注册器。 139 | // 140 | // 返回值: 141 | // - Options: 包含所有传入配置信息的 Options 结构体实例。 142 | func ProvideKratosOptions( 143 | grpcServer *grpc.Server, 144 | httpServer *http.Server, 145 | registrar registry.Registrar, 146 | ) Options { 147 | // 返回一个初始化后的 Options 结构体实例,将传入的参数赋值给对应的字段 148 | return Options{ 149 | GRPCServer: grpcServer, 150 | HTTPServer: httpServer, 151 | Registrar: registrar, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/log/banner.txt: -------------------------------------------------------------------------------- 1 | (ˉˉ) (ˉ\/ˉ)(ˉˉ(ˉ\(ˉ\/ˉ) 2 | / (_/\ ) / / / ) ( 3 | \____/(__/ \_)__)(_/\_) v1.2.2 -------------------------------------------------------------------------------- /app/log/conf/log.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.db; 4 | 5 | option go_package = "github.com/go-lynx/lynx/log/conf"; 6 | 7 | // Defines a message type for log system configuration. 8 | // 定义一个用于日志系统配置的消息类型。 9 | message log { 10 | // The log level: debug, info, warn, error, etc. 11 | // 日志级别:debug、info、warn、error 等。 12 | string level = 1; 13 | 14 | // The file path where logs should be written. 15 | // 日志输出的文件路径。 16 | string file_path = 2; 17 | 18 | // Whether to also output logs to the console. 19 | // 是否同时输出日志到控制台。 20 | bool console_output = 3; 21 | 22 | // The maximum size of a single log file before rotation. 23 | // 单个日志文件的最大大小,超过该大小将触发轮转。 24 | int32 max_size_mb = 4; 25 | 26 | // The maximum number of backup log files to keep. 27 | // 最多保留的旧日志文件数。 28 | int32 max_backups = 5; 29 | 30 | // The maximum number of days to retain old log files. 31 | // 日志文件最多保留的天数。 32 | int32 max_age_days = 6; 33 | 34 | // Whether to compress rotated log files. 35 | // 是否压缩轮转后的日志文件。 36 | bool compress = 7; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/subscribe/router.go: -------------------------------------------------------------------------------- 1 | package subscribe 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/selector" 5 | "github.com/go-lynx/lynx/app" 6 | ) 7 | 8 | // nodeFilter 方法为 GrpcSubscribe 结构体生成一个 selector.NodeFilter 实例。 9 | // 若控制平面不可用,则返回 nil;否则,调用控制平面的方法创建一个新的节点路由器。 10 | func (g *GrpcSubscribe) nodeFilter() selector.NodeFilter { 11 | // 检查应用的控制平面是否为 nil 12 | if app.Lynx().GetControlPlane() == nil { 13 | // 若控制平面为 nil,返回 nil 表示不使用节点过滤器 14 | return nil 15 | } 16 | // 若控制平面存在,调用其 NewNodeRouter 方法创建一个新的节点路由器 17 | return app.Lynx().GetControlPlane().NewNodeRouter(g.svcName) 18 | } 19 | -------------------------------------------------------------------------------- /app/subscribe/subscribe.go: -------------------------------------------------------------------------------- 1 | package subscribe 2 | 3 | import ( 4 | "context" 5 | "github.com/go-kratos/kratos/v2/middleware/logging" 6 | "github.com/go-kratos/kratos/v2/middleware/tracing" 7 | "github.com/go-kratos/kratos/v2/registry" 8 | "github.com/go-kratos/kratos/v2/transport/grpc" 9 | "github.com/go-lynx/lynx/app/log" 10 | gGrpc "google.golang.org/grpc" 11 | ) 12 | 13 | // GrpcSubscribe 表示一个用于订阅 GRPC 服务的结构体。 14 | // 包含服务名称、服务发现实例、是否启用 TLS、根 CA 文件名和文件组等信息。 15 | type GrpcSubscribe struct { 16 | svcName string // 要订阅的 GRPC 服务的名称 17 | discovery registry.Discovery // 服务发现实例,用于发现服务节点 18 | tls bool // 是否启用 TLS 加密 19 | caName string // 根 CA 证书的文件名 20 | caGroup string // 根 CA 证书文件所属的组 21 | required bool // 是否强依赖的上游服务,启动时会做检查 22 | } 23 | 24 | // Option 定义一个函数类型,用于配置 GrpcSubscribe 实例。 25 | type Option func(o *GrpcSubscribe) 26 | 27 | // WithServiceName 返回一个 Option 函数,用于设置要订阅的 GRPC 服务的名称。 28 | func WithServiceName(svcName string) Option { 29 | return func(o *GrpcSubscribe) { 30 | o.svcName = svcName 31 | } 32 | } 33 | 34 | // WithDiscovery 返回一个 Option 函数,用于设置服务发现实例。 35 | func WithDiscovery(discovery registry.Discovery) Option { 36 | return func(o *GrpcSubscribe) { 37 | o.discovery = discovery 38 | } 39 | } 40 | 41 | // EnableTls 返回一个 Option 函数,用于启用 TLS 加密。 42 | func EnableTls() Option { 43 | return func(o *GrpcSubscribe) { 44 | o.tls = true 45 | } 46 | } 47 | 48 | // WithRootCAFileName 返回一个 Option 函数,用于设置根 CA 证书的文件名。 49 | func WithRootCAFileName(caName string) Option { 50 | return func(o *GrpcSubscribe) { 51 | o.caName = caName 52 | } 53 | } 54 | 55 | // WithRootCAFileGroup 返回一个 Option 函数,用于设置根 CA 证书文件所属的组。 56 | func WithRootCAFileGroup(caGroup string) Option { 57 | return func(o *GrpcSubscribe) { 58 | o.caGroup = caGroup 59 | } 60 | } 61 | 62 | // Required 返回一个 Option 函数,用于设置服务为强依赖的上游服务。 63 | func Required() Option { 64 | return func(o *GrpcSubscribe) { 65 | o.required = true 66 | } 67 | } 68 | 69 | // NewGrpcSubscribe 使用提供的选项创建一个新的 GrpcSubscribe 实例。 70 | // 如果没有提供选项,将使用默认配置。 71 | func NewGrpcSubscribe(opts ...Option) *GrpcSubscribe { 72 | gs := &GrpcSubscribe{ 73 | tls: false, // 默认不启用 TLS 加密 74 | } 75 | // 应用提供的选项配置 76 | for _, o := range opts { 77 | o(gs) 78 | } 79 | return gs 80 | } 81 | 82 | // Subscribe 订阅指定的 GRPC 服务,并返回一个 gGrpc.ClientConn 连接实例。 83 | // 如果服务名称为空,则返回 nil。 84 | func (g *GrpcSubscribe) Subscribe() *gGrpc.ClientConn { 85 | if g.svcName == "" { 86 | return nil 87 | } 88 | // 配置 gRPC 客户端选项 89 | opts := []grpc.ClientOption{ 90 | grpc.WithEndpoint("discovery:///" + g.svcName), // 设置服务发现的端点 91 | grpc.WithDiscovery(g.discovery), // 设置服务发现实例 92 | grpc.WithMiddleware( 93 | logging.Client(log.Logger), // 添加日志中间件 94 | tracing.Client(), // 添加链路追踪中间件 95 | ), 96 | grpc.WithTLSConfig(g.tlsLoad()), // 设置 TLS 配置 97 | grpc.WithNodeFilter(g.nodeFilter()), // 设置节点过滤器 98 | } 99 | var conn *gGrpc.ClientConn 100 | var err error 101 | if g.tls { 102 | // 启用 TLS 时,使用安全连接 103 | conn, err = grpc.Dial(context.Background(), opts...) 104 | } else { 105 | // 未启用 TLS 时,使用非安全连接 106 | conn, err = grpc.DialInsecure(context.Background(), opts...) 107 | } 108 | if err != nil { 109 | // 记录错误日志并抛出异常 110 | log.Error(err) 111 | panic(err) 112 | } 113 | return conn 114 | } 115 | -------------------------------------------------------------------------------- /app/subscribe/tls.go: -------------------------------------------------------------------------------- 1 | package subscribe 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "github.com/go-kratos/kratos/v2/config" 7 | "github.com/go-lynx/lynx/app" 8 | "github.com/go-lynx/lynx/app/tls/conf" 9 | ) 10 | 11 | // tlsLoad 方法用于加载 TLS 配置。如果未启用 TLS,则返回 nil。 12 | // 该方法会尝试获取根证书并将其添加到证书池中,最终返回一个配置好的 tls.Config 实例。 13 | func (g *GrpcSubscribe) tlsLoad() *tls.Config { 14 | // 检查是否启用 TLS,如果未启用则直接返回 nil 15 | if !g.tls { 16 | return nil 17 | } 18 | 19 | // 创建一个新的证书池,用于存储根证书 20 | certPool := x509.NewCertPool() 21 | var rootCA []byte 22 | 23 | // 检查是否指定了根 CA 证书的名称 24 | if g.caName != "" { 25 | // Obtain the root certificate of the remote file 26 | // 检查应用的控制平面是否可用,如果不可用则返回 nil 27 | if app.Lynx().GetControlPlane() == nil { 28 | return nil 29 | } 30 | // if group is empty, use the name as the group name. 31 | // 如果未指定根 CA 证书文件所属的组,则使用根 CA 证书的名称作为组名 32 | if g.caGroup == "" { 33 | g.caGroup = g.caName 34 | } 35 | // 从控制平面获取配置信息 36 | s, err := app.Lynx().GetControlPlane().GetConfig(g.caName, g.caGroup) 37 | if err != nil { 38 | // 若获取配置信息失败,则触发 panic 39 | panic(err) 40 | } 41 | // 创建一个新的配置实例,并将从控制平面获取的配置源设置进去 42 | c := config.New( 43 | config.WithSource(s), 44 | ) 45 | // 加载配置信息 46 | if err := c.Load(); err != nil { 47 | // 若加载配置信息失败,则触发 panic 48 | panic(err) 49 | } 50 | // 定义一个 Cert 结构体变量,用于存储从配置中扫描出的证书信息 51 | var t conf.Cert 52 | // 将配置信息扫描到 Cert 结构体变量中 53 | if err := c.Scan(&t); err != nil { 54 | // 若扫描配置信息失败,则触发 panic 55 | panic(err) 56 | } 57 | // 将从配置中获取的根 CA 证书信息转换为字节切片 58 | rootCA = []byte(t.GetRootCA()) 59 | } else { 60 | // Use the root certificate of the current application directly 61 | // 若未指定根 CA 证书的名称,则直接使用当前应用的根证书 62 | rootCA = app.Lynx().Certificate().GetRootCACertificate() 63 | } 64 | // 将根证书添加到证书池中,如果添加失败则触发 panic 65 | if !certPool.AppendCertsFromPEM(rootCA) { 66 | panic("Failed to load root certificate") 67 | } 68 | // 返回配置好的 TLS 配置实例,设置服务器名称和根证书池 69 | return &tls.Config{ServerName: g.caName, RootCAs: certPool} 70 | } 71 | -------------------------------------------------------------------------------- /app/tls/conf/tls.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.tls; 4 | 5 | option go_package = "github.com/go-lynx/plugin-tls/conf;conf"; 6 | 7 | // Tls 消息定义了 TLS 相关配置信息。 8 | message Tls { 9 | // file_name 表示 TLS 配置文件的名称,用于指定配置文件的存储位置。 10 | string file_name = 1; 11 | // group 表示 TLS 配置所属的组,可用于对不同的 TLS 配置进行分类管理。 12 | string group = 2; 13 | } 14 | 15 | // Cert 消息定义了 TLS 证书相关信息。 16 | message Cert { 17 | // crt 表示 X.509 证书文件的路径,通常为 PEM 格式。 18 | string crt = 1; 19 | // key 表示与证书对应的私钥文件的路径,通常为 PEM 格式。 20 | string key = 2; 21 | // rootCA 表示根证书颁发机构(CA)的证书文件路径,用于验证客户端或服务器身份。 22 | string rootCA = 3; 23 | } 24 | -------------------------------------------------------------------------------- /app/tls/tls.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | _ "database/sql" 5 | "github.com/go-kratos/kratos/v2/config" 6 | "github.com/go-lynx/lynx/app" 7 | "github.com/go-lynx/lynx/app/log" 8 | "github.com/go-lynx/lynx/app/tls/conf" 9 | "github.com/go-lynx/lynx/plugins" 10 | ) 11 | 12 | // LoaderTls represents the TLS certificate loader plugin 13 | type LoaderTls struct { 14 | *plugins.BasePlugin 15 | tls *conf.Tls 16 | cert *conf.Cert 17 | weight int 18 | } 19 | 20 | // GetCertificate returns the TLS/SSL certificate in PEM format 21 | func (t *LoaderTls) GetCertificate() []byte { 22 | return []byte(t.cert.GetCrt()) 23 | } 24 | 25 | // GetPrivateKey returns the private key in PEM format 26 | func (t *LoaderTls) GetPrivateKey() []byte { 27 | return []byte(t.cert.GetKey()) 28 | } 29 | 30 | // GetRootCACertificate returns the root CA certificate in PEM format 31 | func (t *LoaderTls) GetRootCACertificate() []byte { 32 | return []byte(t.cert.GetRootCA()) 33 | } 34 | 35 | // Option defines the function type for plugin options 36 | type Option func(t *LoaderTls) 37 | 38 | // Weight sets the plugin weight 39 | func Weight(w int) Option { 40 | return func(t *LoaderTls) { 41 | t.weight = w 42 | } 43 | } 44 | 45 | // Config sets the TLS configuration 46 | func Config(tls *conf.Tls) Option { 47 | return func(t *LoaderTls) { 48 | t.tls = tls 49 | } 50 | } 51 | 52 | // InitializeResources implements custom initialization for TLS loader plugin 53 | func (t *LoaderTls) InitializeResources(rt plugins.Runtime) error { 54 | if t.tls == nil { 55 | t.tls = &conf.Tls{ 56 | FileName: "", 57 | Group: "", 58 | } 59 | } 60 | err := rt.GetConfig().Scan(t.tls) 61 | if err != nil { 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | // StartupTasks performs necessary tasks during plugin startup 68 | func (t *LoaderTls) StartupTasks() error { 69 | if t.tls.GetFileName() == "" { 70 | return nil 71 | } 72 | log.Infof("TLS Certificate Loading") 73 | cfg, err := app.Lynx().GetControlPlane().GetConfig(t.tls.GetFileName(), t.tls.GetGroup()) 74 | if err != nil { 75 | return err 76 | } 77 | c := config.New(config.WithSource(cfg)) 78 | if err := c.Load(); err != nil { 79 | return err 80 | } 81 | 82 | err = c.Scan(t.cert) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | app.Lynx().SetCertificateProvider(t) 88 | log.Infof("TLS Certificate Loaded successfully") 89 | return nil 90 | } 91 | 92 | // CleanupTasks implements custom cleanup logic for TLS loader plugin 93 | func (t *LoaderTls) CleanupTasks() error { 94 | app.Lynx().SetCertificateProvider(nil) 95 | return nil 96 | } 97 | 98 | // NewTlsLoader creates a new TLS loader plugin instance 99 | func NewTlsLoader(opts ...Option) plugins.Plugin { 100 | t := &LoaderTls{ 101 | weight: 100, 102 | tls: &conf.Tls{}, 103 | cert: &conf.Cert{}, 104 | } 105 | for _, opt := range opts { 106 | opt(t) 107 | } 108 | return t 109 | } 110 | -------------------------------------------------------------------------------- /app/util/bcrypt.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "golang.org/x/crypto/bcrypt" 5 | ) 6 | 7 | // HashEncryption 对明文进行加密操作,使用 bcrypt 算法。 8 | // 参数 plaintext 是需要加密的明文。 9 | // 返回值为加密后的字符串和可能出现的错误。 10 | func HashEncryption(plaintext string, cost int) (string, error) { 11 | // 检查 cost 值是否在 bcrypt 的有效范围内 12 | if cost < 4 || cost > 31 { 13 | cost = 10 14 | } 15 | // 使用 bcrypt.GenerateFromPassword 函数对明文进行加密,第二个参数 10 是 cost 值,控制加密的计算复杂度。 16 | bytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), cost) 17 | // 将加密后的字节切片转换为字符串返回 18 | return string(bytes), err 19 | } 20 | 21 | // CheckCiphertext 对明文和密文进行校验,验证明文是否与密文匹配。 22 | // 参数 plaintext 是需要验证的明文,ciphertext 是用于比对的密文。 23 | // 返回值为布尔类型,表示校验是否通过。 24 | func CheckCiphertext(plaintext, ciphertext string) bool { 25 | // 使用 bcrypt.CompareHashAndPassword 函数比较明文和密文是否匹配 26 | err := bcrypt.CompareHashAndPassword([]byte(ciphertext), []byte(plaintext)) 27 | // 若 err 为 nil 则表示匹配成功,返回 true;否则返回 false 28 | return err == nil 29 | } 30 | -------------------------------------------------------------------------------- /app/util/bcrypt_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // TestBcrypt 进行 bcrypt 加密和解密校验的单元测试 9 | func TestBcrypt(t *testing.T) { 10 | // 定义明文密码 11 | plaintext := "123" 12 | // 调用 HashEncryption 函数对明文密码进行加密 13 | encryption, err := HashEncryption(plaintext, 10) 14 | // 检查加密过程中是否出现错误 15 | if err != nil { 16 | // 若出现错误,使用 panic 终止程序并输出错误信息 17 | panic(err) 18 | } 19 | // 打印加密后的密文 20 | fmt.Printf("密文:" + encryption + "\n") 21 | // 调用 CheckCiphertext 函数对明文和密文进行校验 22 | check := CheckCiphertext(plaintext, encryption) 23 | // 检查校验结果是否为 false 24 | if !check { 25 | // 若校验失败,使用 panic 终止程序并输出错误信息 26 | panic("check error") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/util/jwt.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | 6 | "github.com/golang-jwt/jwt/v5" 7 | ) 8 | 9 | type CustomClaims interface { 10 | Init() error 11 | Valid() error 12 | Decoration() error 13 | jwt.Claims 14 | } 15 | 16 | // Sign 方法用于生成一个 JWT 令牌 17 | func Sign(c CustomClaims, alg string, key *ecdsa.PrivateKey) (string, error) { 18 | // 初始化自定义声明 19 | err := c.Init() 20 | // 如果初始化失败,返回空字符串和错误信息 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | // 验证自定义声明 26 | err = c.Valid() 27 | // 如果验证失败,返回空字符串和错误信息 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | // 创建一个新的 JWT 对象,使用指定的签名算法和自定义声明 33 | t := jwt.NewWithClaims(jwt.GetSigningMethod(alg), c) 34 | // 使用指定的私钥对 JWT 进行签名,并返回签名后的字符串 35 | return t.SignedString(key) 36 | } 37 | 38 | // Check 方法用于验证一个 JWT 令牌的有效性 39 | func Check(token string, c CustomClaims, key ecdsa.PublicKey) (bool, error) { 40 | // 解析 JWT 令牌,并将自定义声明绑定到解析结果上 41 | parse, err := jwt.ParseWithClaims(token, c, func(token *jwt.Token) (interface{}, error) { 42 | // 返回用于验证签名的公钥 43 | return &key, nil 44 | }) 45 | // 如果发生错误,返回 false 和错误信息 46 | if err != nil { 47 | return false, err 48 | } 49 | // 对自定义声明进行装饰 50 | err = c.Decoration() 51 | // 如果发生错误,返回 false 和错误信息 52 | if err != nil { 53 | return false, err 54 | } 55 | // 返回解析结果是否有效 56 | return parse.Valid, nil 57 | } 58 | -------------------------------------------------------------------------------- /app/util/jwt_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "fmt" 10 | "testing" 11 | ) 12 | 13 | func TestJwtTokenSigning(t *testing.T) { 14 | // Generate a key 15 | key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | // Convert the key to a PEM 21 | keyBytes, err := x509.MarshalECPrivateKey(key) 22 | if err != nil { 23 | panic(err) 24 | } 25 | keyPem := pem.EncodeToMemory(&pem.Block{ 26 | Type: "EC PRIVATE KEY", 27 | Bytes: keyBytes, 28 | }) 29 | 30 | fmt.Println("Private key:") 31 | fmt.Println(string(keyPem)) 32 | 33 | // Reverse parse the private key 34 | privateBlock, _ := pem.Decode(keyPem) 35 | if privateBlock == nil { 36 | panic("failed to parse PEM block containing the public key") 37 | } 38 | privateKey, err := x509.ParseECPrivateKey(privateBlock.Bytes) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | // Convert the public key to a PEM 44 | pubKeyBytes, err := x509.MarshalPKIXPublicKey(&key.PublicKey) 45 | if err != nil { 46 | panic(err) 47 | } 48 | pubKeyPem := pem.EncodeToMemory(&pem.Block{ 49 | Type: "PUBLIC KEY", 50 | Bytes: pubKeyBytes, 51 | }) 52 | 53 | fmt.Println("Public key:") 54 | fmt.Println(string(pubKeyPem)) 55 | 56 | // Sign a JWT token 57 | signing, err := Sign(&LoginClaims{ 58 | Id: 123, 59 | Nickname: "老王", 60 | }, "ES256", privateKey) 61 | if err != nil { 62 | panic(err) 63 | } 64 | fmt.Printf("JWT private key signing: %s\n", signing) 65 | 66 | // Reverse parse the public key 67 | block, _ := pem.Decode(pubKeyPem) 68 | if block == nil { 69 | panic("failed to parse PEM block containing the public key") 70 | } 71 | pubKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | pubKey, ok := pubKeyInterface.(*ecdsa.PublicKey) 77 | if !ok { 78 | panic("cannot parse public key") 79 | } 80 | 81 | // Verify the JWT token 82 | l := &LoginClaims{} 83 | check, err := Check(signing, l, *pubKey) 84 | if check { 85 | fmt.Printf("JWT public key verification: %d\n", l.Id) 86 | } 87 | if err != nil { 88 | panic(err) 89 | } 90 | } 91 | 92 | // LoginClaims represents the claims in a JWT token for user login 93 | // LoginClaims 表示用户登录的 JWT 令牌中的声明信息 94 | type LoginClaims struct { 95 | CustomClaims 96 | Id int64 `json:"id"` 97 | Nickname string `json:"nickname"` 98 | } 99 | -------------------------------------------------------------------------------- /boot/conf.go: -------------------------------------------------------------------------------- 1 | package boot 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-kratos/kratos/v2/config" 7 | "github.com/go-kratos/kratos/v2/config/file" 8 | "github.com/go-kratos/kratos/v2/log" 9 | ) 10 | 11 | // LoadLocalBootstrapConfig 从本地文件或目录加载引导配置。 12 | // 它从 flagConf 指定的路径读取配置,并初始化应用程序的配置状态。 13 | // 14 | // 返回值: 15 | // - error: 配置加载过程中发生的任何错误 16 | func (b *Boot) LoadLocalBootstrapConfig() error { 17 | // 检查 Boot 实例是否为 nil 18 | if b == nil { 19 | return fmt.Errorf("boot instance is nil") 20 | } 21 | 22 | // 检查配置路径是否为空 23 | if flagConf == "" { 24 | return fmt.Errorf("configuration path is empty") 25 | } 26 | 27 | // 记录尝试加载配置的日志 28 | log.Infof("loading local bootstrap configuration from: %s", flagConf) 29 | 30 | // 从本地文件创建配置源 31 | source := file.NewSource(flagConf) 32 | // 检查配置源是否创建成功 33 | if source == nil { 34 | return fmt.Errorf("failed to create configuration source from: %s", flagConf) 35 | } 36 | 37 | // 创建新的配置实例 38 | cfg := config.New( 39 | // 指定配置源 40 | config.WithSource(source), 41 | ) 42 | // 检查配置实例是否创建成功 43 | if cfg == nil { 44 | return fmt.Errorf("failed to create configuration instance") 45 | } 46 | 47 | // 加载配置 48 | if err := cfg.Load(); err != nil { 49 | return fmt.Errorf("failed to load configuration: %w", err) 50 | } 51 | 52 | // 在设置清理操作之前存储配置 53 | // 这样可以确保如果清理设置失败,不会丢失配置引用 54 | b.conf = cfg 55 | 56 | // 设置配置清理操作 57 | if err := b.setupConfigCleanup(cfg); err != nil { 58 | // 记录清理设置失败的日志,但继续执行 59 | // 配置仍然有效且可用 60 | log.Warnf("Failed to setup configuration cleanup: %v", err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // setupConfigCleanup 确保在配置资源不再需要时进行正确的清理。 67 | func (b *Boot) setupConfigCleanup(cfg config.Config) error { 68 | // 检查配置实例是否为 nil 69 | if cfg == nil { 70 | return fmt.Errorf("configuration instance is nil") 71 | } 72 | 73 | // 设置延迟清理函数 74 | b.cleanup = func() { 75 | // 关闭配置资源 76 | if err := cfg.Close(); err != nil { 77 | // 记录关闭配置失败的日志 78 | log.Errorf("failed to close configuration: %v", err) 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /boot/strap.go: -------------------------------------------------------------------------------- 1 | package boot 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "github.com/go-kratos/kratos/v2/encoding/json" 8 | "github.com/go-lynx/lynx/app/log" 9 | "google.golang.org/protobuf/encoding/protojson" 10 | "time" 11 | 12 | "github.com/go-kratos/kratos/v2" 13 | "github.com/go-kratos/kratos/v2/config" 14 | kratoslog "github.com/go-kratos/kratos/v2/log" 15 | "github.com/go-lynx/lynx/app" 16 | "github.com/go-lynx/lynx/plugins" 17 | ) 18 | 19 | // flagConf 存储从命令行参数中获取的配置文件路径 20 | var ( 21 | // flagConf holds the configuration file path from command line arguments 22 | flagConf string 23 | ) 24 | 25 | // Boot 表示 Lynx 应用程序的主要引导结构,负责管理应用的初始化、配置加载和生命周期 26 | type Boot struct { 27 | wire wireApp // 用于初始化 Kratos 应用程序的函数 28 | plugins []plugins.Plugin // 要初始化的插件列表 29 | conf config.Config // 应用程序的配置实例 30 | cleanup func() // 清理函数,用于在应用关闭时执行资源清理操作 31 | } 32 | 33 | // init 包初始化函数,用于解析命令行参数并配置 JSON 序列化选项 34 | func init() { 35 | // 从命令行参数中获取配置文件路径,默认值为 "../../configs" 36 | flag.StringVar(&flagConf, "conf", "../../configs", "config path, eg: -conf config.yaml") 37 | flag.Parse() 38 | } 39 | 40 | // wireApp 是一个函数类型,用于初始化并返回一个 Kratos 应用程序实例 41 | type wireApp func(logger kratoslog.Logger) (*kratos.App, error) 42 | 43 | // Run 启动 Lynx 应用程序并管理其生命周期 44 | func (b *Boot) Run() error { 45 | // 检查 Boot 实例是否为 nil 46 | if b == nil { 47 | return fmt.Errorf("boot instance is nil") 48 | } 49 | 50 | // 延迟执行 panic 处理和清理操作 51 | defer b.handlePanic() 52 | if b.cleanup != nil { 53 | defer b.cleanup() 54 | } 55 | 56 | // 记录应用启动时间,用于计算启动耗时 57 | startTime := time.Now() 58 | 59 | // 加载引导配置 60 | if err := b.LoadLocalBootstrapConfig(); err != nil { 61 | return fmt.Errorf("failed to load bootstrap configuration: %w", err) 62 | } 63 | 64 | // 初始化 Lynx 应用程序 65 | lynxApp, err := app.NewApp(b.conf, b.plugins...) 66 | if err != nil { 67 | return fmt.Errorf("failed to create Lynx application: %w", err) 68 | } 69 | 70 | // 初始化日志记录器 71 | if err := log.InitLogger(app.GetName(), app.GetHost(), app.GetVersion(), b.conf); err != nil { 72 | return fmt.Errorf("failed to initialize logger: %w", err) 73 | } 74 | 75 | // 记录应用启动信息 76 | log.Info("lynx application is starting up") 77 | 78 | // 获取插件管理器 79 | pluginManager := lynxApp.GetPluginManager() 80 | if pluginManager == nil { 81 | return fmt.Errorf("plugin manager is nil") 82 | } 83 | 84 | // 加载插件 85 | pluginManager.LoadPlugins(b.conf) 86 | 87 | // 初始化 Kratos 应用程序 88 | kratosApp, err := b.wire(log.Logger) 89 | if err != nil { 90 | log.Error(err) 91 | return fmt.Errorf("failed to initialize Kratos application: %w", err) 92 | } 93 | 94 | // 配置 protocol buffers 的 JSON 序列化选项 95 | jsonEmit, jsonConfErr := lynxApp.GetGlobalConfig().Value("lynx.http.response.json.emitUnpopulated").Bool() 96 | if jsonConfErr != nil && errors.As(config.ErrNotFound, &jsonConfErr) { 97 | jsonEmit = false 98 | } 99 | // EmitUnpopulated: 序列化时包含未设置的字段 100 | // UseProtoNames: 使用 proto 文件中定义的字段名 101 | json.MarshalOptions = protojson.MarshalOptions{ 102 | EmitUnpopulated: jsonEmit, 103 | UseProtoNames: true, 104 | } 105 | 106 | // 计算应用启动耗时 107 | elapsedMs := time.Since(startTime).Milliseconds() 108 | var elapsedDisplay string 109 | switch { 110 | case elapsedMs < 1000: 111 | // 小于1秒,显示毫秒 112 | elapsedDisplay = fmt.Sprintf("%d ms", elapsedMs) 113 | case elapsedMs < 60_000: 114 | // 小于1分钟,显示秒(保留两位小数) 115 | elapsedDisplay = fmt.Sprintf("%.2f s", float64(elapsedMs)/1000) 116 | default: 117 | // 1分钟以上,显示分钟(保留两位小数) 118 | elapsedDisplay = fmt.Sprintf("%.2f m", float64(elapsedMs)/1000/60) 119 | } 120 | log.Infof("lynx application started successfully, elapsed time: %s, port listening initiated", elapsedDisplay) 121 | 122 | // 运行 Kratos 应用程序 123 | if err := kratosApp.Run(); err != nil { 124 | log.Error(err) 125 | return fmt.Errorf("failed to run Kratos application: %w", err) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | // handlePanic 用于从 panic 中恢复,并确保资源的正确清理 132 | func (b *Boot) handlePanic() { 133 | // 捕获 panic 134 | if r := recover(); r != nil { 135 | var err error 136 | // 根据 panic 的类型转换为 error 137 | switch v := r.(type) { 138 | case error: 139 | err = v 140 | case string: 141 | err = fmt.Errorf(v) 142 | default: 143 | err = fmt.Errorf("%v", r) 144 | } 145 | log.Error(err) 146 | 147 | lynxApp := app.Lynx() 148 | // 确保插件被卸载 149 | if lynxApp != nil && lynxApp.GetPluginManager() != nil { 150 | lynxApp.GetPluginManager().UnloadPlugins() 151 | } 152 | } 153 | } 154 | 155 | // NewLynxApplication 创建一个新的 Lynx 微服务引导程序实例 156 | // 参数: 157 | // - wire: 用于初始化 Kratos 应用程序的函数 158 | // - plugins: 可选的插件列表,用于随应用一起初始化 159 | // 160 | // 返回值: 161 | // - *Boot: 初始化后的 Boot 实例 162 | func NewLynxApplication(wire wireApp, plugins ...plugins.Plugin) *Boot { 163 | // 检查 wire 函数是否为 nil 164 | if wire == nil { 165 | log.Error("wire function cannot be nil") 166 | return nil 167 | } 168 | 169 | // 返回初始化后的 Boot 实例 170 | return &Boot{ 171 | wire: wire, 172 | plugins: plugins, 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /cmd/lynx/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/cmd/lynx 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.7 7 | github.com/fatih/color v1.16.0 8 | github.com/spf13/cobra v1.8.0 9 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 10 | ) 11 | 12 | require ( 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 15 | github.com/mattn/go-colorable v0.1.13 // indirect 16 | github.com/mattn/go-isatty v0.0.20 // indirect 17 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 18 | github.com/spf13/pflag v1.0.5 // indirect 19 | golang.org/x/sys v0.14.0 // indirect 20 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 21 | golang.org/x/text v0.4.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /cmd/lynx/internal/base/mod.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/mod/modfile" 7 | ) 8 | 9 | // ModulePath 从指定的 go.mod 文件中提取 Go 模块路径。 10 | // 参数 filename 是 go.mod 文件的路径。 11 | // 返回值: 12 | // - string: 提取到的 Go 模块路径。 13 | // - error: 若读取文件或解析模块路径时出错,则返回相应的错误信息;否则返回 nil。 14 | func ModulePath(filename string) (string, error) { 15 | // 读取指定路径的 go.mod 文件内容 16 | modBytes, err := os.ReadFile(filename) 17 | if err != nil { 18 | // 若读取文件失败,返回空字符串和错误信息 19 | return "", err 20 | } 21 | // 调用 modfile.ModulePath 函数从文件内容中提取 Go 模块路径 22 | return modfile.ModulePath(modBytes), nil 23 | } 24 | -------------------------------------------------------------------------------- /cmd/lynx/internal/base/path.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | // lynxHome 获取 Lynx 工具的主目录。 15 | // 若主目录不存在,则创建该目录。 16 | // 返回 Lynx 工具主目录的路径。 17 | func lynxHome() string { 18 | // 获取当前用户的主目录 19 | dir, err := os.UserHomeDir() 20 | if err != nil { 21 | // 若获取失败,记录错误并终止程序 22 | log.Fatal(err) 23 | } 24 | // 拼接 Lynx 工具主目录的路径 25 | home := filepath.Join(dir, ".lynx") 26 | // 检查主目录是否存在 27 | if _, err := os.Stat(home); os.IsNotExist(err) { 28 | // 若不存在,则递归创建目录 29 | if err := os.MkdirAll(home, 0o700); err != nil { 30 | // 若创建失败,记录错误并终止程序 31 | log.Fatal(err) 32 | } 33 | } 34 | return home 35 | } 36 | 37 | // lynxHomeWithDir 获取 Lynx 工具主目录下指定子目录的路径。 38 | // 若子目录不存在,则创建该目录。 39 | // 参数 dir 是指定的子目录名称。 40 | // 返回 Lynx 工具主目录下指定子目录的路径。 41 | func lynxHomeWithDir(dir string) string { 42 | // 拼接 Lynx 工具主目录下指定子目录的路径 43 | home := filepath.Join(lynxHome(), dir) 44 | // 检查子目录是否存在 45 | if _, err := os.Stat(home); os.IsNotExist(err) { 46 | // 若不存在,则递归创建目录 47 | if err := os.MkdirAll(home, 0o700); err != nil { 48 | // 若创建失败,记录错误并终止程序 49 | log.Fatal(err) 50 | } 51 | } 52 | return home 53 | } 54 | 55 | // copyFile 将源文件复制到目标文件,并根据替换规则替换文件内容。 56 | // 参数 src 是源文件路径,dst 是目标文件路径,replaces 是替换规则列表,格式为 [old1, new1, old2, new2, ...]。 57 | // 返回复制过程中可能出现的错误。 58 | func copyFile(src, dst string, replaces []string) error { 59 | // 获取源文件的信息 60 | srcInfo, err := os.Stat(src) 61 | if err != nil { 62 | return err 63 | } 64 | // 读取源文件的内容 65 | buf, err := os.ReadFile(src) 66 | if err != nil { 67 | return err 68 | } 69 | var old string 70 | // 遍历替换规则列表 71 | for i, next := range replaces { 72 | if i%2 == 0 { 73 | // 偶数索引的元素为旧字符串 74 | old = next 75 | continue 76 | } 77 | // 奇数索引的元素为新字符串,进行全局替换 78 | buf = bytes.ReplaceAll(buf, []byte(old), []byte(next)) 79 | } 80 | // 将替换后的内容写入目标文件,并保持文件权限不变 81 | return os.WriteFile(dst, buf, srcInfo.Mode()) 82 | } 83 | 84 | // copyDir 递归复制源目录到目标目录,并根据替换规则替换文件内容,同时忽略指定的文件或目录。 85 | // 参数 src 是源目录路径,dst 是目标目录路径,replaces 是替换规则列表,ignores 是需要忽略的文件或目录列表。 86 | // 返回复制过程中可能出现的错误。 87 | func copyDir(src, dst string, replaces, ignores []string) error { 88 | // 获取源目录的信息 89 | srcInfo, err := os.Stat(src) 90 | if err != nil { 91 | return err 92 | } 93 | // 递归创建目标目录,并保持目录权限不变 94 | err = os.MkdirAll(dst, srcInfo.Mode()) 95 | if err != nil { 96 | return err 97 | } 98 | // 读取源目录下的所有文件和子目录 99 | fds, err := os.ReadDir(src) 100 | if err != nil { 101 | return err 102 | } 103 | // 遍历源目录下的所有文件和子目录 104 | for _, fd := range fds { 105 | // 检查是否需要忽略当前文件或目录 106 | if hasSets(fd.Name(), ignores) { 107 | continue 108 | } 109 | // 拼接源文件或子目录的完整路径 110 | srcFilePath := filepath.Join(src, fd.Name()) 111 | // 拼接目标文件或子目录的完整路径 112 | dstFilePath := filepath.Join(dst, fd.Name()) 113 | var e error 114 | if fd.IsDir() { 115 | // 若为目录,则递归调用 copyDir 函数 116 | e = copyDir(srcFilePath, dstFilePath, replaces, ignores) 117 | } else { 118 | // 若为文件,则调用 copyFile 函数 119 | e = copyFile(srcFilePath, dstFilePath, replaces) 120 | } 121 | if e != nil { 122 | return e 123 | } 124 | } 125 | return nil 126 | } 127 | 128 | // hasSets 检查指定的名称是否在给定的集合中。 129 | // 参数 name 是要检查的名称,sets 是集合列表。 130 | // 返回布尔值,表示名称是否在集合中。 131 | func hasSets(name string, sets []string) bool { 132 | // 遍历集合列表 133 | for _, ig := range sets { 134 | if ig == name { 135 | return true 136 | } 137 | } 138 | return false 139 | } 140 | 141 | // Tree 打印指定目录下所有文件的创建信息,包括文件名和文件大小。 142 | // 参数 path 是要遍历的目录路径,dir 是基础目录,用于格式化输出路径。 143 | func Tree(path string, dir string) { 144 | // 递归遍历指定目录下的所有文件和子目录 145 | _ = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 146 | // 若没有错误,且文件信息不为空,且不是目录 147 | if err == nil && info != nil && !info.IsDir() { 148 | // 打印文件创建信息,包括文件名和文件大小 149 | fmt.Printf("%s %s (%v bytes)\n", color.GreenString("CREATED"), strings.Replace(path, dir+"/", "", -1), info.Size()) 150 | } 151 | return nil 152 | }) 153 | } 154 | -------------------------------------------------------------------------------- /cmd/lynx/internal/base/repo.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // unExpandVarPath 定义了不需要展开变量的路径前缀列表。 15 | var unExpandVarPath = []string{"~", ".", ".."} 16 | 17 | // Repo 表示 Git 仓库管理器,用于管理仓库的克隆、拉取和复制等操作。 18 | type Repo struct { 19 | url string // 仓库的远程 URL 20 | home string // 仓库的本地缓存路径 21 | branch string // 要操作的仓库分支 22 | } 23 | 24 | // repoDir 根据仓库的 URL 生成一个相对唯一的目录名。 25 | // 参数 url 是仓库的远程 URL。 26 | // 返回值为生成的目录名。 27 | func repoDir(url string) string { 28 | // 解析仓库的 VCS URL 29 | vcsURL, err := ParseVCSUrl(url) 30 | if err != nil { 31 | // 解析失败时直接返回原始 URL 32 | return url 33 | } 34 | // 检查主机名是否包含端口号 35 | host, _, err := net.SplitHostPort(vcsURL.Host) 36 | if err != nil { 37 | // 不包含端口号时,直接使用原始主机名 38 | host = vcsURL.Host 39 | } 40 | // 去除主机名中不需要展开变量的路径前缀 41 | for _, p := range unExpandVarPath { 42 | host = strings.TrimLeft(host, p) 43 | } 44 | // 获取 URL 路径中倒数第二个目录名 45 | dir := path.Base(path.Dir(vcsURL.Path)) 46 | // 组合主机名和目录名作为最终的目录名 47 | url = fmt.Sprintf("%s/%s", host, dir) 48 | return url 49 | } 50 | 51 | // NewRepo 创建一个新的仓库管理器实例。 52 | // 参数 url 是仓库的远程 URL,branch 是要操作的仓库分支。 53 | // 返回值为新创建的 Repo 实例指针。 54 | func NewRepo(url string, branch string) *Repo { 55 | return &Repo{ 56 | url: url, 57 | // 计算仓库的本地缓存路径 58 | home: lynxHomeWithDir("repo/" + repoDir(url)), 59 | branch: branch, 60 | } 61 | } 62 | 63 | // Path 返回仓库的本地缓存路径。 64 | // 返回值为本地缓存路径的字符串。 65 | func (r *Repo) Path() string { 66 | // 找到 URL 中最后一个 '/' 的索引 67 | start := strings.LastIndex(r.url, "/") 68 | // 找到 URL 中最后一个 '.git' 的索引 69 | end := strings.LastIndex(r.url, ".git") 70 | if end == -1 { 71 | // 若没有 '.git',则取 URL 的长度 72 | end = len(r.url) 73 | } 74 | var branch string 75 | if r.branch == "" { 76 | // 若分支名为空,默认使用 '@main' 77 | branch = "@main" 78 | } else { 79 | // 否则,添加 '@' 前缀 80 | branch = "@" + r.branch 81 | } 82 | // 组合本地缓存路径 83 | return path.Join(r.home, r.url[start+1:end]+branch) 84 | } 85 | 86 | // Pull 从远程仓库拉取最新代码到本地缓存路径。 87 | // 参数 ctx 是上下文,用于控制操作的生命周期。 88 | // 返回值为操作过程中可能出现的错误。 89 | func (r *Repo) Pull(ctx context.Context) error { 90 | // 检查本地仓库是否为有效的 Git 仓库 91 | cmd := exec.CommandContext(ctx, "git", "symbolic-ref", "HEAD") 92 | cmd.Dir = r.Path() 93 | _, err := cmd.CombinedOutput() 94 | if err != nil { 95 | return err 96 | } 97 | // 执行 git pull 命令拉取最新代码 98 | cmd = exec.CommandContext(ctx, "git", "pull") 99 | cmd.Dir = r.Path() 100 | out, err := cmd.CombinedOutput() 101 | // 打印命令执行输出 102 | fmt.Println(string(out)) 103 | if err != nil { 104 | return err 105 | } 106 | return err 107 | } 108 | 109 | // Clone 将远程仓库克隆到本地缓存路径。如果本地缓存路径已存在,则尝试拉取最新代码。 110 | // 参数 ctx 是上下文,用于控制操作的生命周期。 111 | // 返回值为操作过程中可能出现的错误。 112 | func (r *Repo) Clone(ctx context.Context) error { 113 | // 检查本地缓存路径是否已存在 114 | if _, err := os.Stat(r.Path()); !os.IsNotExist(err) { 115 | // 若存在,尝试拉取最新代码 116 | return r.Pull(ctx) 117 | } 118 | var cmd *exec.Cmd 119 | if r.branch == "" { 120 | // 若分支名为空,克隆默认分支 121 | cmd = exec.CommandContext(ctx, "git", "clone", r.url, r.Path()) 122 | } else { 123 | // 否则,克隆指定分支 124 | cmd = exec.CommandContext(ctx, "git", "clone", "-b", r.branch, r.url, r.Path()) 125 | } 126 | out, err := cmd.CombinedOutput() 127 | // 打印命令执行输出 128 | fmt.Println(string(out)) 129 | if err != nil { 130 | return err 131 | } 132 | return nil 133 | } 134 | 135 | // CopyTo 将克隆后的仓库内容复制到指定的项目路径。 136 | // 参数 ctx 是上下文,用于控制操作的生命周期;to 是目标项目路径;modPath 是模块路径;ignores 是需要忽略的文件或目录列表。 137 | // 返回值为操作过程中可能出现的错误。 138 | func (r *Repo) CopyTo(ctx context.Context, to string, modPath string, ignores []string) error { 139 | // 先克隆仓库到本地缓存路径 140 | if err := r.Clone(ctx); err != nil { 141 | return err 142 | } 143 | // 获取本地缓存路径下 go.mod 文件中的模块路径 144 | mod, err := ModulePath(filepath.Join(r.Path(), "go.mod")) 145 | if err != nil { 146 | return err 147 | } 148 | // 复制目录内容 149 | return copyDir(r.Path(), to, []string{mod, modPath}, ignores) 150 | } 151 | 152 | // CopyToV2 将克隆后的仓库内容复制到指定的项目路径,支持更多的替换规则。 153 | // 参数 ctx 是上下文,用于控制操作的生命周期;to 是目标项目路径;modPath 是模块路径;ignores 是需要忽略的文件或目录列表;replaces 是需要替换的内容列表。 154 | // 返回值为操作过程中可能出现的错误。 155 | func (r *Repo) CopyToV2(ctx context.Context, to string, modPath string, ignores, replaces []string) error { 156 | // 先克隆仓库到本地缓存路径 157 | if err := r.Clone(ctx); err != nil { 158 | return err 159 | } 160 | // 获取本地缓存路径下 go.mod 文件中的模块路径 161 | mod, err := ModulePath(filepath.Join(r.Path(), "go.mod")) 162 | if err != nil { 163 | return err 164 | } 165 | // 将模块路径和 modPath 添加到替换列表 166 | replaces = append([]string{mod, modPath}, replaces...) 167 | // 复制目录内容 168 | return copyDir(r.Path(), to, replaces, ignores) 169 | } 170 | -------------------------------------------------------------------------------- /cmd/lynx/internal/base/vcs_url.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // scpSyntaxRe 用于匹配 SCP 风格的 URL 语法,例如 "git@github.com:user/repo"。 11 | var ( 12 | scpSyntaxRe = regexp.MustCompile(`^(\w+)@([\w.-]+):(.*)$`) 13 | // scheme 定义了支持的版本控制系统(VCS)URL 协议列表。 14 | scheme = []string{"git", "https", "http", "git+ssh", "ssh", "file", "ftp", "ftps"} 15 | ) 16 | 17 | // ParseVCSUrl 解析版本控制系统(VCS)的仓库 URL。 18 | // 参考 https://github.com/golang/go/blob/master/src/cmd/go/internal/vcs/vcs.go 19 | // 查看 https://go-review.googlesource.com/c/go/+/12226/ 20 | // Git URL 定义参考 https://git-scm.com/docs/git-clone#_git_urls 21 | // 参数 repo 是需要解析的仓库 URL 字符串。 22 | // 返回值为解析后的 *url.URL 实例和可能出现的错误。 23 | func ParseVCSUrl(repo string) (*url.URL, error) { 24 | var ( 25 | repoURL *url.URL 26 | err error 27 | ) 28 | 29 | // 检查是否为 SCP 风格的 URL 语法 30 | if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil { 31 | // 若匹配 SCP 风格的语法,则将其转换为标准 URL 格式。 32 | // 例如,"git@github.com:user/repo" 会转换为 "ssh://git@github.com/user/repo"。 33 | repoURL = &url.URL{ 34 | Scheme: "ssh", 35 | User: url.User(m[1]), 36 | Host: m[2], 37 | Path: m[3], 38 | } 39 | } else { 40 | // 若不是 SCP 风格的语法,确保 URL 包含 "//" 41 | if !strings.Contains(repo, "//") { 42 | repo = "//" + repo 43 | } 44 | // 处理以 "//git@" 开头的 URL,将其转换为 "ssh:" 协议 45 | if strings.HasPrefix(repo, "//git@") { 46 | repo = "ssh:" + repo 47 | } else if strings.HasPrefix(repo, "//") { 48 | // 处理以 "//" 开头的 URL,将其转换为 "https:" 协议 49 | repo = "https:" + repo 50 | } 51 | // 使用标准库的 url.Parse 函数解析 URL 52 | repoURL, err = url.Parse(repo) 53 | if err != nil { 54 | return nil, err 55 | } 56 | } 57 | 58 | // 检查解析后的 URL 协议是否在支持的协议列表中 59 | // 同时也会检查不安全的协议,因为此函数仅用于报告仓库 URL 的状态 60 | for _, s := range scheme { 61 | if repoURL.Scheme == s { 62 | return repoURL, nil 63 | } 64 | } 65 | // 若协议不支持,则返回错误 66 | return nil, errors.New("unable to parse repo url") 67 | } 68 | -------------------------------------------------------------------------------- /cmd/lynx/internal/project/new.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/AlecAivazis/survey/v2" 10 | "github.com/fatih/color" 11 | 12 | "github.com/go-lynx/lynx/cmd/lynx/internal/base" 13 | ) 14 | 15 | // Project 表示一个项目模板,包含项目的名称和路径信息。 16 | type Project struct { 17 | Name string // 项目名称 18 | Path string // 项目路径 19 | } 20 | 21 | // New 从远程仓库创建一个新项目。 22 | // ctx: 上下文,用于控制操作的生命周期。 23 | // dir: 项目创建的目标目录。 24 | // layout: 项目布局的远程仓库地址。 25 | // branch: 要使用的远程仓库分支。 26 | // 返回值: 若操作过程中出现错误,则返回相应的错误信息;否则返回 nil。 27 | func (p *Project) New(ctx context.Context, dir string, layout string, branch string) error { 28 | // 计算项目最终创建的完整路径 29 | to := filepath.Join(dir, p.Name) 30 | 31 | // 检查目标路径是否已存在 32 | if _, err := os.Stat(to); !os.IsNotExist(err) { 33 | // 若存在,提示用户路径已存在 34 | fmt.Printf("🚫 %s already exists\n", p.Name) 35 | // 创建一个确认提示,询问用户是否要覆盖该文件夹 36 | prompt := &survey.Confirm{ 37 | Message: "📂 Do you want to override the folder ?", 38 | Help: "Delete the existing folder and create the project.", 39 | } 40 | var override bool 41 | // 询问用户并将结果存储在 override 变量中 42 | e := survey.AskOne(prompt, &override) 43 | if e != nil { 44 | return e 45 | } 46 | // 若用户不同意覆盖,则返回错误 47 | if !override { 48 | return err 49 | } 50 | // 删除已存在的文件夹 51 | err := os.RemoveAll(to) 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | 57 | // 提示用户开始创建项目,并显示项目名称和布局仓库信息 58 | fmt.Printf("🌟 Creating Lynx service %s, layout repo is %s, please wait a moment.\n\n", p.Name, layout) 59 | // 创建一个新的仓库实例 60 | repo := base.NewRepo(layout, branch) 61 | // 将远程仓库内容复制到目标路径,并排除 .git 和 .github 目录 62 | if err := repo.CopyTo(ctx, to, p.Name, []string{".git", ".github"}); err != nil { 63 | return err 64 | } 65 | // 重命名 cmd 目录下的 user 目录为项目名称 66 | e := os.Rename( 67 | filepath.Join(to, "cmd", "user"), 68 | filepath.Join(to, "cmd", p.Name), 69 | ) 70 | if e != nil { 71 | return e 72 | } 73 | // 打印项目目录结构 74 | base.Tree(to, dir) 75 | 76 | // 提示用户项目创建成功 77 | fmt.Printf("\n🎉 Project creation succeeded %s\n", color.GreenString(p.Name)) 78 | // 提示用户使用以下命令启动项目 79 | fmt.Print("💻 Use the following command to start the project 👇:\n\n") 80 | fmt.Println(color.WhiteString("$ cd %s", p.Name)) 81 | fmt.Println(color.WhiteString("$ go generate ./...")) 82 | fmt.Println(color.WhiteString("$ go build -o ./bin/ ./... ")) 83 | fmt.Println(color.WhiteString("$ ./bin/%s -conf ./configs\n", p.Name)) 84 | // 感谢用户使用 Lynx 并提供教程链接 85 | fmt.Println("🤝 Thanks for using Lynx") 86 | fmt.Println("📚 Tutorial: https://go-lynx.cn/docs/start") 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /cmd/lynx/internal/project/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/AlecAivazis/survey/v2" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | // CmdNew 代表 `new` 命令,用于创建 Lynx 服务模板项目。 19 | var CmdNew = &cobra.Command{ 20 | Use: "new", 21 | Short: "Create a lynx service template", 22 | Long: "Create a lynx service project using the repository template.", 23 | Run: run, // 执行命令时调用的函数 24 | } 25 | 26 | // repoURL 存储布局仓库的 URL。 27 | // branch 存储仓库的分支名称。 28 | // timeout 存储项目创建操作的超时时间。 29 | var ( 30 | repoURL string 31 | branch string 32 | timeout string 33 | ) 34 | 35 | // init 是包的初始化函数,用于设置默认值和命令行标志。 36 | func init() { 37 | // 从环境变量 LYNX_LAYOUT_REPO 获取仓库 URL,若为空则使用默认值 38 | if repoURL = os.Getenv("LYNX_LAYOUT_REPO"); repoURL == "" { 39 | repoURL = "https://github.com/go-lynx/lynx-layout.git" 40 | } 41 | timeout = "60s" // 默认超时时间为 60 秒 42 | // 为命令添加 --repo-url 标志,用于指定布局仓库 URL 43 | CmdNew.Flags().StringVarP(&repoURL, "repo-url", "r", repoURL, "layout repo") 44 | // 为命令添加 --branch 标志,用于指定仓库分支 45 | CmdNew.Flags().StringVarP(&branch, "branch", "b", branch, "repo branch") 46 | // 为命令添加 --timeout 标志,用于指定超时时间 47 | CmdNew.Flags().StringVarP(&timeout, "timeout", "t", timeout, "time out") 48 | } 49 | 50 | // run 是 `new` 命令的执行函数,负责创建 Lynx 服务项目。 51 | func run(_ *cobra.Command, args []string) { 52 | // 获取当前工作目录 53 | wd, err := os.Getwd() 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | // 将超时时间字符串解析为 time.Duration 类型 59 | t, err := time.ParseDuration(timeout) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | // 创建带有超时的上下文 65 | ctx, cancel := context.WithTimeout(context.Background(), t) 66 | defer cancel() // 确保在函数结束时取消上下文 67 | 68 | var names []string 69 | if len(args) == 0 { 70 | // 若未通过命令行参数提供项目名称,则提示用户输入 71 | prompt := &survey.Input{ 72 | Message: "What are the project names ?", 73 | Help: "Enter project names separated by space.", 74 | } 75 | var input string 76 | err = survey.AskOne(prompt, &input) 77 | if err != nil || input == "" { 78 | fmt.Printf("\n❌ No project names found,Please provide the correct project name\n") 79 | return 80 | } 81 | // 将用户输入的项目名称按空格分割 82 | names = strings.Split(input, " ") 83 | } else { 84 | names = args 85 | } 86 | 87 | // 检查并去除重复的项目名称 88 | names = checkDuplicates(names) 89 | if len(names) < 1 { 90 | fmt.Printf("\n❌ No project names found,Please provide the correct project name\n") 91 | return 92 | } 93 | 94 | // 并发创建多个项目 95 | done := make(chan error, len(names)) 96 | var wg sync.WaitGroup 97 | for _, name := range names { 98 | wg.Add(1) 99 | // 处理项目名称和工作目录参数 100 | projectName, workingDir := processProjectParams(name, wd) 101 | p := &Project{Name: projectName} 102 | go func() { 103 | // 调用 Project 的 New 方法创建项目,并将结果发送到 done 通道 104 | done <- p.New(ctx, workingDir, repoURL, branch) 105 | wg.Done() 106 | }() 107 | } 108 | 109 | wg.Wait() // 等待所有 goroutine 完成 110 | close(done) // 关闭 done 通道 111 | 112 | // 从 done 通道读取错误信息并打印 113 | for err := range done { 114 | if err != nil { 115 | _, _ = fmt.Fprintf(os.Stderr, "\033[31mERROR: Failed to create project(%s)\033[m\n", err.Error()) 116 | } 117 | } 118 | // 检查上下文是否因超时而取消 119 | if errors.Is(ctx.Err(), context.DeadlineExceeded) { 120 | _, _ = fmt.Fprint(os.Stderr, "\033[31mERROR: project creation timed out\033[m\n") 121 | return 122 | } 123 | } 124 | 125 | // processProjectParams 处理项目名称参数,返回处理后的项目名称和工作目录。 126 | func processProjectParams(projectName string, workingDir string) (projectNameResult, workingDirResult string) { 127 | _projectDir := projectName 128 | _workingDir := workingDir 129 | // 处理以 ~ 开头的项目名称,将其替换为用户主目录 130 | if strings.HasPrefix(projectName, "~") { 131 | homeDir, err := os.UserHomeDir() 132 | if err != nil { 133 | // 若无法获取用户主目录,则返回原始值 134 | return _projectDir, _workingDir 135 | } 136 | _projectDir = filepath.Join(homeDir, projectName[2:]) 137 | } 138 | 139 | // 检查项目名称是否为相对路径,若是则转换为绝对路径 140 | if !filepath.IsAbs(projectName) { 141 | absPath, err := filepath.Abs(projectName) 142 | if err != nil { 143 | return _projectDir, _workingDir 144 | } 145 | _projectDir = absPath 146 | } 147 | 148 | // 返回处理后的项目名称(路径最后一部分)和工作目录(路径目录部分) 149 | return filepath.Base(_projectDir), filepath.Dir(_projectDir) 150 | } 151 | 152 | // checkDuplicates 检查并去除项目名称列表中的重复项,同时验证名称的合法性。 153 | func checkDuplicates(names []string) []string { 154 | encountered := map[string]bool{} 155 | var result []string 156 | 157 | // 定义项目名称的合法字符模式 158 | pattern := `^[A-Za-z0-9_-]+$` 159 | regex := regexp.MustCompile(pattern) 160 | 161 | for _, name := range names { 162 | // 若名称符合模式且未出现过,则添加到结果列表 163 | if regex.MatchString(name) && !encountered[name] { 164 | encountered[name] = true 165 | result = append(result, name) 166 | } 167 | } 168 | return result 169 | } 170 | -------------------------------------------------------------------------------- /cmd/lynx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-lynx/lynx/cmd/lynx/internal/project" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | // rootCmd 是 Lynx 命令行工具的根命令,定义了工具的基本信息和版本。 10 | var rootCmd = &cobra.Command{ 11 | // Use 定义了命令的使用方式 12 | Use: "lynx", 13 | // Short 是命令的简短描述 14 | Short: "Lynx: The Plug-and-Play Go Microservices Framework", 15 | // Long 是命令的详细描述 16 | Long: `Lynx: The Plug-and-Play Go Microservices Framework`, 17 | // Version 定义了命令行工具的版本,release 变量需在别处定义 18 | Version: release, 19 | } 20 | 21 | // init 函数是包的初始化函数,在包被加载时自动执行。 22 | func init() { 23 | // 为根命令添加子命令,这里添加了 project 包中的 CmdNew 命令 24 | rootCmd.AddCommand(project.CmdNew) 25 | } 26 | 27 | // main 函数是程序的入口点,负责执行根命令。 28 | func main() { 29 | // 执行根命令,如果执行过程中出现错误则记录错误日志并终止程序 30 | if err := rootCmd.Execute(); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/lynx/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // release is the current kratos tool version. 4 | const release = "v1.2.2" 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-kratos/kratos/v2 v2.8.4 7 | github.com/golang-jwt/jwt/v5 v5.2.2 8 | github.com/rs/zerolog v1.34.0 9 | golang.org/x/crypto v0.37.0 10 | google.golang.org/grpc v1.68.1 11 | google.golang.org/protobuf v1.36.5 12 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 13 | ) 14 | 15 | require ( 16 | cel.dev/expr v0.22.0 // indirect 17 | dario.cat/mergo v1.0.0 // indirect 18 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 19 | github.com/fsnotify/fsnotify v1.6.0 // indirect 20 | github.com/go-kratos/aegis v0.2.0 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | github.com/go-playground/form/v4 v4.2.0 // indirect 24 | github.com/google/uuid v1.6.0 // indirect 25 | github.com/gorilla/mux v1.8.1 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.19 // indirect 28 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 29 | go.opentelemetry.io/otel v1.33.0 // indirect 30 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 31 | go.opentelemetry.io/otel/sdk v1.33.0 // indirect 32 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 33 | golang.org/x/net v0.39.0 // indirect 34 | golang.org/x/sync v0.13.0 // indirect 35 | golang.org/x/sys v0.32.0 // indirect 36 | golang.org/x/text v0.24.0 // indirect 37 | google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 38 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /plugins/db/mysql/conf/mysql.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.db; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/db/mysql/conf;conf"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // Defines a message type for MySQL configuration. 10 | // 定义一个用于 MySQL 数据库配置的消息类型。 11 | message mysql { 12 | // The driver name for the MySQL database. 13 | // MySQL 数据库的驱动名称。 14 | string driver = 1; 15 | // The data source name (DSN) for the MySQL database. 16 | // MySQL 数据库的数据源名称(DSN),用于连接数据库。 17 | string source = 2; 18 | // The minimum number of connections to maintain in the connection pool. 19 | // 连接池中需要维持的最小连接数。 20 | int32 min_conn = 3; 21 | // The maximum number of connections to maintain in the connection pool. 22 | // 连接池中需要维持的最大连接数。 23 | int32 max_conn = 4; 24 | // The maximum lifetime for a connection in the connection pool. 25 | // 连接池中连接的最大生命时间,超过该时间的连接可能会被关闭。 26 | google.protobuf.Duration max_life_time = 5; 27 | // The maximum idle time for a connection in the connection pool. 28 | // 连接池中连接的最大空闲时间,超过该时间的连接可能会被关闭。 29 | google.protobuf.Duration max_idle_time = 6; 30 | } 31 | -------------------------------------------------------------------------------- /plugins/db/mysql/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/db/mysql 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | entgo.io/ent v0.14.4 7 | github.com/go-lynx/lynx v1.2.1 8 | github.com/go-sql-driver/mysql v1.9.2 9 | google.golang.org/protobuf v1.35.2 10 | ) 11 | 12 | require ( 13 | dario.cat/mergo v1.0.0 // indirect 14 | filippo.io/edwards25519 v1.1.0 // indirect 15 | github.com/go-kratos/aegis v0.2.0 // indirect 16 | github.com/go-kratos/kratos/v2 v2.8.4 // indirect 17 | github.com/go-logr/logr v1.4.2 // indirect 18 | github.com/go-logr/stdr v1.2.2 // indirect 19 | github.com/go-playground/form/v4 v4.2.0 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/gorilla/mux v1.8.1 // indirect 22 | github.com/mattn/go-colorable v0.1.13 // indirect 23 | github.com/mattn/go-isatty v0.0.19 // indirect 24 | github.com/rs/zerolog v1.34.0 // indirect 25 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 26 | go.opentelemetry.io/otel v1.33.0 // indirect 27 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 28 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 29 | golang.org/x/sync v0.13.0 // indirect 30 | golang.org/x/sys v0.32.0 // indirect 31 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 32 | google.golang.org/grpc v1.68.1 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /plugins/db/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "context" 5 | _ "database/sql" 6 | "time" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | 10 | esql "entgo.io/ent/dialect/sql" 11 | "github.com/go-lynx/lynx/app/log" 12 | "github.com/go-lynx/lynx/plugins" 13 | "github.com/go-lynx/lynx/plugins/db/mysql/conf" 14 | "google.golang.org/protobuf/types/known/durationpb" 15 | ) 16 | 17 | // Plugin metadata 18 | // 插件元数据,包含插件名称、版本、描述和配置前缀 19 | const ( 20 | // 插件名称 21 | pluginName = "mysql.client" 22 | // 插件版本 23 | pluginVersion = "v2.0.0" 24 | // 插件描述 25 | pluginDescription = "mysql client plugin for lynx framework" 26 | // 配置前缀 27 | confPrefix = "lynx.mysql" 28 | ) 29 | 30 | // DBMysqlClient 表示 MySQL 客户端插件实例 31 | type DBMysqlClient struct { 32 | // 继承基础插件 33 | *plugins.BasePlugin 34 | // 数据库驱动 35 | dri *esql.Driver 36 | // MySQL 配置 37 | conf *conf.Mysql 38 | } 39 | 40 | // NewMysqlClient 创建一个新的 MySQL 客户端插件实例 41 | // 返回一个指向 DBMysqlClient 结构体的指针 42 | func NewMysqlClient() *DBMysqlClient { 43 | return &DBMysqlClient{ 44 | BasePlugin: plugins.NewBasePlugin( 45 | // 生成插件 ID 46 | plugins.GeneratePluginID("", pluginName, pluginVersion), 47 | // 插件名称 48 | pluginName, 49 | // 插件描述 50 | pluginDescription, 51 | // 插件版本 52 | pluginVersion, 53 | // 配置前缀 54 | confPrefix, 55 | // 权重 56 | 101, 57 | ), 58 | } 59 | } 60 | 61 | // InitializeResources 从运行时配置中扫描并加载 MySQL 配置 62 | // 参数 rt 为运行时环境 63 | // 返回错误信息,如果配置加载失败则返回相应错误 64 | func (m *DBMysqlClient) InitializeResources(rt plugins.Runtime) error { 65 | // 初始化一个空的配置结构 66 | m.conf = &conf.Mysql{} 67 | 68 | // 从运行时配置中扫描并加载 MySQL 配置 69 | err := rt.GetConfig().Value(confPrefix).Scan(m.conf) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // 设置默认配置 75 | defaultConf := &conf.Mysql{ 76 | Driver: "mysql", 77 | Source: "root:123456@tcp(127.0.0.1:3306)/db_name?charset=utf8mb4&parseTime=True&loc=Local", 78 | MinConn: 10, 79 | MaxConn: 20, 80 | MaxIdleTime: &durationpb.Duration{Seconds: 10}, 81 | MaxLifeTime: &durationpb.Duration{Seconds: 300}, 82 | } 83 | 84 | // 对未设置的字段使用默认值 85 | if m.conf.Driver == "" { 86 | m.conf.Driver = defaultConf.Driver 87 | } 88 | if m.conf.Source == "" { 89 | m.conf.Source = defaultConf.Source 90 | } 91 | if m.conf.MinConn == 0 { 92 | m.conf.MinConn = defaultConf.MinConn 93 | } 94 | if m.conf.MaxConn == 0 { 95 | m.conf.MaxConn = defaultConf.MaxConn 96 | } 97 | if m.conf.MaxIdleTime == nil { 98 | m.conf.MaxIdleTime = defaultConf.MaxIdleTime 99 | } 100 | if m.conf.MaxLifeTime == nil { 101 | m.conf.MaxLifeTime = defaultConf.MaxLifeTime 102 | } 103 | 104 | return nil 105 | } 106 | 107 | // StartupTasks 初始化数据库连接并进行健康检查 108 | // 返回错误信息,如果连接或健康检查失败则返回相应错误 109 | func (m *DBMysqlClient) StartupTasks() error { 110 | // 记录数据库初始化日志 111 | log.Infof("Initializing database") 112 | 113 | // 打开数据库连接 114 | drv, err := esql.Open( 115 | m.conf.Driver, 116 | m.conf.Source, 117 | ) 118 | 119 | if err != nil { 120 | // 记录打开数据库连接失败日志 121 | log.Errorf("failed opening connection to dataBase: %v", err) 122 | // 发生错误时 panic 123 | panic(err) 124 | } 125 | 126 | // 设置连接池的最大空闲连接数 127 | drv.DB().SetMaxIdleConns(int(m.conf.MinConn)) 128 | // 设置连接池的最大打开连接数 129 | drv.DB().SetMaxOpenConns(int(m.conf.MaxConn)) 130 | // 设置连接的最大空闲时间 131 | drv.DB().SetConnMaxIdleTime(m.conf.MaxIdleTime.AsDuration()) 132 | // 设置连接的最大生命周期 133 | drv.DB().SetConnMaxLifetime(m.conf.MaxLifeTime.AsDuration()) 134 | 135 | // 将数据库驱动赋值给实例 136 | m.dri = drv 137 | // 记录数据库初始化成功日志 138 | log.Infof("database successfully initialized") 139 | // 原代码此处返回值有误,正确返回 nil 140 | return nil 141 | } 142 | 143 | // CleanupTasks 关闭数据库连接 144 | // 返回错误信息,如果关闭连接失败则返回相应错误 145 | func (m *DBMysqlClient) CleanupTasks() error { 146 | if m.dri == nil { 147 | return nil 148 | } 149 | // 关闭数据库连接 150 | if err := m.dri.Close(); err != nil { 151 | // 记录关闭数据库连接失败日志 152 | log.Error(err) 153 | return err 154 | } 155 | // 记录关闭数据库资源日志 156 | log.Info("message", "Closing the DataBase resources") 157 | return nil 158 | } 159 | 160 | // Configure 更新 HTTP 服务器的配置。 161 | // 该函数接收一个任意类型的参数,尝试将其转换为 *conf.Http 类型,如果转换成功则更新配置。 162 | func (m *DBMysqlClient) Configure(c any) error { 163 | // 尝试将传入的配置转换为 *conf.Http 类型 164 | if mysqlConf, ok := c.(*conf.Mysql); ok { 165 | // 转换成功,更新配置 166 | m.conf = mysqlConf 167 | return nil 168 | } 169 | // 转换失败,返回配置无效错误 170 | return plugins.ErrInvalidConfiguration 171 | } 172 | 173 | // CheckHealth 对 HTTP 服务器进行健康检查。 174 | // 该函数目前直接返回 nil,表示服务器健康,可根据实际需求添加检查逻辑。 175 | func (m *DBMysqlClient) CheckHealth() error { 176 | // 创建一个带超时的上下文 177 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 178 | defer cancel() 179 | // 执行数据库连接健康检查 180 | err := m.dri.DB().PingContext(ctx) 181 | if err != nil { 182 | // 原代码此处返回值有误,正确返回错误信息 183 | return err 184 | } 185 | return nil 186 | } 187 | -------------------------------------------------------------------------------- /plugins/db/mysql/plug.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "entgo.io/ent/dialect/sql" 5 | "github.com/go-lynx/lynx/app" 6 | "github.com/go-lynx/lynx/app/factory" 7 | "github.com/go-lynx/lynx/plugins" 8 | ) 9 | 10 | // init 是 Go 语言的初始化函数,在包被加载时自动执行。 11 | // 此函数的作用是将 MySQL 客户端插件注册到全局插件工厂中。 12 | func init() { 13 | // 获取全局插件工厂实例,并调用其 RegisterPlugin 方法进行插件注册。 14 | // 第一个参数 pluginName 是插件的名称,用于唯一标识该插件。 15 | // 第二个参数 confPrefix 是配置前缀,用于从配置中读取插件相关配置。 16 | // 第三个参数是一个匿名函数,该函数返回一个 plugins.Plugin 接口类型的实例, 17 | // 这里调用 NewMysqlClient 函数创建一个新的 MySQL 客户端插件实例。 18 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 19 | return NewMysqlClient() 20 | }) 21 | } 22 | 23 | // GetDriver 函数用于获取 MySQL 客户端的数据库驱动实例。 24 | // 返回值为 *sql.Driver 类型,即数据库驱动指针。 25 | func GetDriver() *sql.Driver { 26 | // 从全局 Lynx 应用实例中获取插件管理器, 27 | // 再通过插件管理器根据插件名称获取对应的插件实例, 28 | // 最后将获取到的插件实例转换为 *DBMysqlClient 类型, 29 | // 并返回其 dri 字段,即数据库驱动实例。 30 | plugin := app.Lynx().GetPluginManager().GetPlugin(pluginName) 31 | if plugin == nil { 32 | return nil 33 | } 34 | return plugin.(*DBMysqlClient).dri 35 | } 36 | -------------------------------------------------------------------------------- /plugins/db/pgsql/conf/pgsql.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.db; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/db/pgsql/conf;conf"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // Defines a message type for PgSQL configuration. 10 | // 定义一个用于 PgSQL 数据库配置的消息类型。 11 | message pgsql { 12 | // The driver name for the PgSQL database. 13 | // PgSQL 数据库的驱动名称。 14 | string driver = 1; 15 | // The data source name (DSN) for the PgSQL database. 16 | // PgSQL 数据库的数据源名称(DSN),用于连接数据库。 17 | string source = 2; 18 | // The minimum number of connections to maintain in the connection pool. 19 | // 连接池中需要维持的最小连接数。 20 | int32 min_conn = 3; 21 | // The maximum number of connections to maintain in the connection pool. 22 | // 连接池中需要维持的最大连接数。 23 | int32 max_conn = 4; 24 | // The maximum lifetime for a connection in the connection pool. 25 | // 连接池中连接的最大生命时间,超过该时间的连接可能会被关闭。 26 | google.protobuf.Duration max_life_time = 5; 27 | // The maximum idle time for a connection in the connection pool. 28 | // 连接池中连接的最大空闲时间,超过该时间的连接可能会被关闭。 29 | google.protobuf.Duration max_idle_time = 6; 30 | } 31 | -------------------------------------------------------------------------------- /plugins/db/pgsql/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/db/pgsql 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | entgo.io/ent v0.14.4 7 | github.com/go-lynx/lynx v1.2.1 8 | github.com/jackc/pgx/v5 v5.7.5 9 | google.golang.org/protobuf v1.35.2 10 | ) 11 | 12 | require ( 13 | dario.cat/mergo v1.0.0 // indirect 14 | github.com/go-kratos/aegis v0.2.0 // indirect 15 | github.com/go-kratos/kratos/v2 v2.8.4 // indirect 16 | github.com/go-logr/logr v1.4.2 // indirect 17 | github.com/go-logr/stdr v1.2.2 // indirect 18 | github.com/go-playground/form/v4 v4.2.0 // indirect 19 | github.com/google/uuid v1.6.0 // indirect 20 | github.com/gorilla/mux v1.8.1 // indirect 21 | github.com/jackc/pgpassfile v1.0.0 // indirect 22 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 23 | github.com/jackc/puddle/v2 v2.2.2 // indirect 24 | github.com/mattn/go-colorable v0.1.13 // indirect 25 | github.com/mattn/go-isatty v0.0.19 // indirect 26 | github.com/rs/zerolog v1.34.0 // indirect 27 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 28 | go.opentelemetry.io/otel v1.33.0 // indirect 29 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 30 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 31 | golang.org/x/crypto v0.37.0 // indirect 32 | golang.org/x/sync v0.13.0 // indirect 33 | golang.org/x/sys v0.32.0 // indirect 34 | golang.org/x/text v0.24.0 // indirect 35 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 36 | google.golang.org/grpc v1.68.1 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /plugins/db/pgsql/pgsql.go: -------------------------------------------------------------------------------- 1 | package pgsql 2 | 3 | import ( 4 | "context" 5 | sql "database/sql" 6 | 7 | stdlib "github.com/jackc/pgx/v5/stdlib" 8 | 9 | "time" 10 | 11 | esql "entgo.io/ent/dialect/sql" 12 | "github.com/go-lynx/lynx/app/log" 13 | "github.com/go-lynx/lynx/plugins" 14 | "github.com/go-lynx/lynx/plugins/db/pgsql/conf" 15 | "google.golang.org/protobuf/types/known/durationpb" 16 | ) 17 | 18 | // Plugin metadata 19 | // 插件元数据,包含插件名称、版本、描述和配置前缀 20 | const ( 21 | // 插件名称 22 | pluginName = "pgsql.client" 23 | // 插件版本 24 | pluginVersion = "v2.0.0" 25 | // 插件描述 26 | pluginDescription = "pgsql client plugin for lynx framework" 27 | // 配置前缀 28 | confPrefix = "lynx.pgsql" 29 | ) 30 | 31 | // DBPgsqlClient 表示 PgSQL 客户端插件实例 32 | type DBPgsqlClient struct { 33 | // 继承基础插件 34 | *plugins.BasePlugin 35 | // 数据库驱动 36 | dri *esql.Driver 37 | // PgSQL 配置 38 | conf *conf.Pgsql 39 | } 40 | 41 | // NewPgsqlClient 创建一个新的 PgSQL 客户端插件实例 42 | // 返回一个指向 DBPgsqlClient 结构体的指针 43 | func NewPgsqlClient() *DBPgsqlClient { 44 | return &DBPgsqlClient{ 45 | BasePlugin: plugins.NewBasePlugin( 46 | // 生成插件 ID 47 | plugins.GeneratePluginID("", pluginName, pluginVersion), 48 | pluginName, 49 | // 插件描述 50 | pluginDescription, 51 | // 插件版本 52 | pluginVersion, 53 | // 配置前缀 54 | confPrefix, 55 | // 权重 56 | 101, 57 | ), 58 | } 59 | } 60 | 61 | // InitializeResources 从运行时配置中扫描并加载 PgSQL 配置 62 | // 参数 rt 为运行时环境 63 | // 返回错误信息,如果配置加载失败则返回相应错误 64 | func (p *DBPgsqlClient) InitializeResources(rt plugins.Runtime) error { 65 | // 初始化一个空的配置结构 66 | p.conf = &conf.Pgsql{} 67 | 68 | // 从运行时配置中扫描并加载 PgSQL 配置 69 | err := rt.GetConfig().Value(confPrefix).Scan(p.conf) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // 设置默认配置 75 | defaultConf := &conf.Pgsql{ 76 | Driver: "postgres", 77 | Source: "postgres://admin:123456@127.0.0.1:5432/demo?sslmode=disable", 78 | MinConn: 10, 79 | MaxConn: 20, 80 | MaxIdleTime: &durationpb.Duration{Seconds: 10}, 81 | MaxLifeTime: &durationpb.Duration{Seconds: 300}, 82 | } 83 | 84 | // 对未设置的字段使用默认值 85 | if p.conf.Driver == "" { 86 | p.conf.Driver = defaultConf.Driver 87 | } 88 | if p.conf.Source == "" { 89 | p.conf.Source = defaultConf.Source 90 | } 91 | if p.conf.MinConn == 0 { 92 | p.conf.MinConn = defaultConf.MinConn 93 | } 94 | if p.conf.MaxConn == 0 { 95 | p.conf.MaxConn = defaultConf.MaxConn 96 | } 97 | if p.conf.MaxIdleTime == nil { 98 | p.conf.MaxIdleTime = defaultConf.MaxIdleTime 99 | } 100 | if p.conf.MaxLifeTime == nil { 101 | p.conf.MaxLifeTime = defaultConf.MaxLifeTime 102 | } 103 | 104 | return nil 105 | } 106 | 107 | // StartupTasks 初始化数据库连接并进行健康检查 108 | // 返回错误信息,如果连接或健康检查失败则返回相应错误 109 | func (p *DBPgsqlClient) StartupTasks() error { 110 | // 记录数据库初始化日志 111 | log.Infof("initializing database") 112 | 113 | // 注册数据库驱动 114 | sql.Register("postgres", stdlib.GetDefaultDriver()) 115 | 116 | // 打开数据库连接 117 | drv, err := esql.Open( 118 | p.conf.Driver, 119 | p.conf.Source, 120 | ) 121 | 122 | if err != nil { 123 | // 记录打开数据库连接失败日志 124 | log.Errorf("failed opening connection to dataBase: %v", err) 125 | // 发生错误时 panic 126 | panic(err) 127 | } 128 | 129 | // 设置连接池的最大空闲连接数 130 | drv.DB().SetMaxIdleConns(int(p.conf.MinConn)) 131 | // 设置连接池的最大打开连接数 132 | drv.DB().SetMaxOpenConns(int(p.conf.MaxConn)) 133 | // 设置连接的最大空闲时间 134 | drv.DB().SetConnMaxIdleTime(p.conf.MaxIdleTime.AsDuration()) 135 | // 设置连接的最大生命周期 136 | drv.DB().SetConnMaxLifetime(p.conf.MaxLifeTime.AsDuration()) 137 | 138 | // 将数据库驱动赋值给实例 139 | p.dri = drv 140 | // 记录数据库初始化成功日志 141 | log.Infof("database successfully initialized") 142 | // 原代码此处返回值有误,正确返回 nil 143 | return nil 144 | } 145 | 146 | // CleanupTasks 关闭数据库连接 147 | // 返回错误信息,如果关闭连接失败则返回相应错误 148 | func (p *DBPgsqlClient) CleanupTasks() error { 149 | if p.dri == nil { 150 | return nil 151 | } 152 | // 关闭数据库连接 153 | if err := p.dri.Close(); err != nil { 154 | // 记录关闭数据库连接失败日志 155 | log.Error(err) 156 | return err 157 | } 158 | // 记录关闭数据库资源日志 159 | log.Info("message", "Closing the DataBase resources") 160 | return nil 161 | } 162 | 163 | // Configure 更新 HTTP 服务器的配置。 164 | // 该函数接收一个任意类型的参数,尝试将其转换为 *conf.Http 类型,如果转换成功则更新配置。 165 | func (p *DBPgsqlClient) Configure(c any) error { 166 | // 尝试将传入的配置转换为 *conf.Http 类型 167 | if mysqlConf, ok := c.(*conf.Pgsql); ok { 168 | // 转换成功,更新配置 169 | p.conf = mysqlConf 170 | return nil 171 | } 172 | // 转换失败,返回配置无效错误 173 | return plugins.ErrInvalidConfiguration 174 | } 175 | 176 | // CheckHealth 对 HTTP 服务器进行健康检查。 177 | // 该函数目前直接返回 nil,表示服务器健康,可根据实际需求添加检查逻辑。 178 | func (p *DBPgsqlClient) CheckHealth() error { 179 | // 创建一个带超时的上下文 180 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 181 | defer cancel() 182 | // 执行数据库连接健康检查 183 | err := p.dri.DB().PingContext(ctx) 184 | if err != nil { 185 | // 原代码此处返回值有误,正确返回错误信息 186 | return err 187 | } 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /plugins/db/pgsql/plug.go: -------------------------------------------------------------------------------- 1 | package pgsql 2 | 3 | import ( 4 | "entgo.io/ent/dialect/sql" 5 | "github.com/go-lynx/lynx/app" 6 | "github.com/go-lynx/lynx/app/factory" 7 | "github.com/go-lynx/lynx/plugins" 8 | ) 9 | 10 | // init 是 Go 语言的初始化函数,在包被加载时自动执行。 11 | // 此函数的作用是将 PgSQL 客户端插件注册到全局插件工厂中。 12 | func init() { 13 | // 获取全局插件工厂实例,并调用其 RegisterPlugin 方法进行插件注册。 14 | // 第一个参数 pluginName 是插件的名称,用于唯一标识该插件。 15 | // 第二个参数 confPrefix 是配置前缀,用于从配置中读取插件相关配置。 16 | // 第三个参数是一个匿名函数,该函数返回一个 plugins.Plugin 接口类型的实例, 17 | // 这里调用 NewMysqlClient 函数创建一个新的 PgSQL 客户端插件实例。 18 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 19 | return NewPgsqlClient() 20 | }) 21 | } 22 | 23 | // GetDriver 函数用于获取 PgSQL 客户端的数据库驱动实例。 24 | // 返回值为 *sql.Driver 类型,即数据库驱动指针。 25 | func GetDriver() *sql.Driver { 26 | // 从全局 Lynx 应用实例中获取插件管理器, 27 | // 再通过插件管理器根据插件名称获取对应的插件实例, 28 | // 最后将获取到的插件实例转换为 *DBMysqlClient 类型, 29 | // 并返回其 dri 字段,即数据库驱动实例。 30 | plugin := app.Lynx().GetPluginManager().GetPlugin(pluginName) 31 | if plugin == nil { 32 | return nil 33 | } 34 | return plugin.(*DBPgsqlClient).dri 35 | } 36 | -------------------------------------------------------------------------------- /plugins/deps.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | // Dependency describes a dependency relationship between plugins 4 | // Defines requirements and relationships between plugins 5 | // Dependency 描述了插件之间的依赖关系。 6 | // 定义了插件之间的需求和关系。 7 | type Dependency struct { 8 | ID string // Unique identifier of the required plugin // 所需插件的唯一标识符 9 | Required bool // Whether this dependency is mandatory // 此依赖项是否为必需项 10 | Checker DependencyChecker // Validates dependency requirements // 验证依赖项要求 11 | Metadata map[string]any // Additional dependency information // 额外的依赖项信息 12 | } 13 | 14 | // DependencyChecker defines the interface for dependency validation 15 | // Validates plugin dependencies and their conditions 16 | // DependencyChecker 定义了依赖项验证的接口。 17 | // 验证插件依赖项及其条件。 18 | type DependencyChecker interface { 19 | // Check validates if the dependency condition is met 20 | // Returns true if the dependency is satisfied 21 | // Check 验证依赖项条件是否满足。 22 | // 如果依赖项满足,则返回 true。 23 | Check(plugin Plugin) bool 24 | 25 | // Description returns a human-readable description of the condition 26 | // Explains what the dependency checker validates 27 | // Description 返回条件的易读描述。 28 | // 解释依赖项检查器验证的内容。 29 | Description() string 30 | } 31 | -------------------------------------------------------------------------------- /plugins/health.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | // HealthReport represents the detailed health status of a plugin 4 | // Provides comprehensive health information for monitoring 5 | // HealthReport 表示插件的详细健康状态。 6 | // 提供全面的健康信息用于监控。 7 | type HealthReport struct { 8 | Status string // Current health status (healthy, degraded, unhealthy) // 当前健康状态(健康、降级、不健康) 9 | Details map[string]any // Detailed health metrics and information // 详细的健康指标和信息 10 | Timestamp int64 // Time of the health check (Unix timestamp) // 健康检查的时间(Unix 时间戳) 11 | Message string // Optional descriptive message // 可选的描述性消息 12 | } 13 | 14 | // HealthCheck defines methods for plugin health monitoring 15 | // Provides health status and monitoring capabilities 16 | // HealthCheck 定义了插件健康监控的方法。 17 | // 提供健康状态和监控功能。 18 | type HealthCheck interface { 19 | // GetHealth returns the current health status of the plugin 20 | // Provides detailed health information 21 | // GetHealth 返回插件的当前健康状态。 22 | // 提供详细的健康信息。 23 | GetHealth() HealthReport 24 | } 25 | -------------------------------------------------------------------------------- /plugins/id.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // ID format constants 10 | // ID 格式常量 11 | const ( 12 | // DefaultOrg is the default organization identifier 13 | // DefaultOrg 是默认的组织标识符 14 | DefaultOrg = "go-lynx" 15 | // ComponentType represents the plugin component type 16 | // ComponentType 表示插件组件类型 17 | ComponentType = "plugin" 18 | ) 19 | 20 | // IDFormat represents the components of a plugin ID 21 | // IDFormat 表示插件 ID 的各个组成部分 22 | type IDFormat struct { 23 | Organization string // e.g., "go-lynx" // 例如,"go-lynx" 24 | Type string // e.g., "plugin" // 例如,"plugin" 25 | Name string // e.g., "http" // 例如,"http" 26 | Version string // e.g., "v1" or "v1.0.0" // 例如,"v1" 或 "v1.0.0" 27 | } 28 | 29 | // ParsePluginID parses a plugin ID string into its components 30 | // ParsePluginID 将插件 ID 字符串解析为其各个组成部分 31 | func ParsePluginID(id string) (*IDFormat, error) { 32 | // 使用点号分割插件 ID 字符串 33 | parts := strings.Split(id, ".") 34 | // 检查分割后的部分数量是否为 4,如果不是则返回无效插件 ID 错误 35 | if len(parts) != 4 { 36 | return nil, ErrInvalidPluginID 37 | } 38 | 39 | // 初始化 IDFormat 结构体 40 | format := &IDFormat{ 41 | Organization: parts[0], 42 | Type: parts[1], 43 | Name: parts[2], 44 | Version: parts[3], 45 | } 46 | 47 | // 验证插件 ID 格式是否正确 48 | if err := ValidatePluginID(id); err != nil { 49 | return nil, err 50 | } 51 | 52 | return format, nil 53 | } 54 | 55 | // GeneratePluginID generates a standard format plugin ID 56 | // GeneratePluginID 生成标准格式的插件 ID 57 | func GeneratePluginID(org, name, version string) string { 58 | // 如果组织名称为空,则使用默认组织名称 59 | if org == "" { 60 | org = DefaultOrg 61 | } 62 | 63 | // 确保版本号以 'v' 开头,如果不是则添加 'v' 64 | if !strings.HasPrefix(version, "v") { 65 | version = "v" + version 66 | } 67 | 68 | // 按照标准格式拼接插件 ID 69 | return fmt.Sprintf("%s.%s.%s.%s", org, ComponentType, name, version) 70 | } 71 | 72 | // ValidatePluginID validates the format of a plugin ID 73 | // ValidatePluginID 验证插件 ID 的格式 74 | func ValidatePluginID(id string) error { 75 | // 正则表达式模式解释: 76 | // ^ 字符串开始 77 | // [\w-]+ 组织名称(单词字符和连字符) 78 | // \.plugin\. 字面量 ".plugin." 79 | // [a-z0-9-]+ 插件名称(小写字母、数字、连字符) 80 | // \.v\d+ 以 'v' 开头的版本号 81 | // (?:\.\d+\.\d+)? 可选的补丁版本号(例如,.0.0) 82 | // $ 字符串结束 83 | pattern := `^[\w-]+\.plugin\.[a-z0-9-]+\.v\d+(?:\.\d+\.\d+)?$` 84 | 85 | // 使用正则表达式匹配插件 ID 86 | match, _ := regexp.MatchString(pattern, id) 87 | if !match { 88 | return ErrInvalidPluginID 89 | } 90 | return nil 91 | } 92 | 93 | // GetPluginMainVersion extracts the main version number from a plugin ID 94 | // GetPluginMainVersion 从插件 ID 中提取主版本号 95 | func GetPluginMainVersion(id string) (string, error) { 96 | // 解析插件 ID 97 | format, err := ParsePluginID(id) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | // 按点号分割版本号,提取主版本号(例如从 v1.0.0 中提取 v1) 103 | parts := strings.Split(format.Version, ".") 104 | return parts[0], nil 105 | } 106 | 107 | // IsPluginVersionCompatible checks if two plugin versions are compatible 108 | // IsPluginVersionCompatible 检查两个插件版本是否兼容 109 | func IsPluginVersionCompatible(v1, v2 string) bool { 110 | // 获取两个插件版本的主版本号 111 | v1Main, err1 := GetPluginMainVersion(v1) 112 | v2Main, err2 := GetPluginMainVersion(v2) 113 | 114 | // 如果获取主版本号过程中出现错误,则认为版本不兼容 115 | if err1 != nil || err2 != nil { 116 | return false 117 | } 118 | 119 | // 比较两个主版本号是否相同 120 | return v1Main == v2Main 121 | } 122 | -------------------------------------------------------------------------------- /plugins/mq/kafka/conf/kafka.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.kafka; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/mq/kafka/conf;conf"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // Kafka 消息定义了 Kafka 客户端的配置信息 10 | message Kafka { 11 | // brokers 表示 Kafka 集群的地址列表 12 | repeated string brokers = 1; 13 | 14 | // producer 生产者配置 15 | Producer producer = 2; 16 | 17 | // consumer 消费者配置 18 | Consumer consumer = 3; 19 | 20 | // 通用配置 21 | // sasl 认证配置 22 | SASL sasl = 4; 23 | // tls 配置 24 | bool tls = 5; 25 | // 连接超时时间 26 | google.protobuf.Duration dial_timeout = 6; 27 | } 28 | 29 | // Producer 生产者配置 30 | message Producer { 31 | // 是否启用生产者 32 | bool enabled = 1; 33 | // 是否需要等待所有副本确认 34 | bool required_acks = 2; 35 | // 最大重试次数 36 | int32 max_retries = 3; 37 | // 重试间隔 38 | google.protobuf.Duration retry_backoff = 4; 39 | // 批量发送大小 40 | int32 batch_size = 5; 41 | // 批量发送等待时间 42 | google.protobuf.Duration batch_timeout = 6; 43 | // 压缩类型:none, gzip, snappy, lz4, zstd 44 | string compression = 7; 45 | } 46 | 47 | // Consumer 消费者配置 48 | message Consumer { 49 | // 是否启用消费者 50 | bool enabled = 1; 51 | // 消费组 ID 52 | string group_id = 2; 53 | // 自动提交间隔 54 | google.protobuf.Duration auto_commit_interval = 3; 55 | // 是否自动提交 56 | bool auto_commit = 4; 57 | // 消费起始位置:latest, earliest 58 | string start_offset = 5; 59 | // 最大处理并发数 60 | int32 max_concurrency = 6; 61 | // 最小批量大小 62 | int32 min_batch_size = 7; 63 | // 最大批量大小 64 | int32 max_batch_size = 8; 65 | // 最大等待时间 66 | google.protobuf.Duration max_wait_time = 9; 67 | // 重平衡超时时间 68 | google.protobuf.Duration rebalance_timeout = 10; 69 | } 70 | 71 | // SASL 认证配置 72 | message SASL { 73 | // 是否启用 SASL 74 | bool enabled = 1; 75 | // 认证机制:PLAIN, SCRAM-SHA-256, SCRAM-SHA-512 76 | string mechanism = 2; 77 | // 用户名 78 | string username = 3; 79 | // 密码 80 | string password = 4; 81 | } 82 | -------------------------------------------------------------------------------- /plugins/mq/kafka/errors.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrProducerNotInitialized = errors.New("kafka producer not initialized") 7 | ErrConsumerNotInitialized = errors.New("kafka consumer not initialized") 8 | ) 9 | -------------------------------------------------------------------------------- /plugins/mq/kafka/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/mq/kafka 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-lynx/lynx v1.2.1 7 | github.com/twmb/franz-go v1.15.3 8 | google.golang.org/protobuf v1.35.2 9 | ) 10 | 11 | require ( 12 | dario.cat/mergo v1.0.0 // indirect 13 | github.com/go-kratos/aegis v0.2.0 // indirect 14 | github.com/go-kratos/kratos/v2 v2.8.4 // indirect 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/go-logr/stdr v1.2.2 // indirect 17 | github.com/go-playground/form/v4 v4.2.0 // indirect 18 | github.com/google/uuid v1.6.0 // indirect 19 | github.com/gorilla/mux v1.8.1 // indirect 20 | github.com/klauspost/compress v1.17.7 // indirect 21 | github.com/mattn/go-colorable v0.1.13 // indirect 22 | github.com/mattn/go-isatty v0.0.19 // indirect 23 | github.com/pierrec/lz4/v4 v4.1.19 // indirect 24 | github.com/rs/zerolog v1.34.0 // indirect 25 | github.com/twmb/franz-go/pkg/kmsg v1.7.0 // indirect 26 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 27 | go.opentelemetry.io/otel v1.33.0 // indirect 28 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 29 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 30 | golang.org/x/crypto v0.37.0 // indirect 31 | golang.org/x/sync v0.13.0 // indirect 32 | golang.org/x/sys v0.32.0 // indirect 33 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 34 | google.golang.org/grpc v1.68.1 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /plugins/mq/kafka/plug.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "github.com/go-lynx/lynx/app/factory" 5 | "github.com/go-lynx/lynx/plugins" 6 | ) 7 | 8 | func init() { 9 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 10 | return NewKafkaClient() 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /plugins/mq/kafka/pool.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import "sync" 4 | 5 | // GoroutinePool 是一个简单的 goroutine 池实现 6 | type GoroutinePool struct { 7 | wg sync.WaitGroup 8 | ch chan struct{} 9 | } 10 | 11 | // NewGoroutinePool 创建一个新的 goroutine 池 12 | func NewGoroutinePool(size int) *GoroutinePool { 13 | return &GoroutinePool{ 14 | ch: make(chan struct{}, size), 15 | } 16 | } 17 | 18 | // Submit 提交一个任务到池中执行 19 | func (p *GoroutinePool) Submit(task func()) { 20 | p.wg.Add(1) 21 | p.ch <- struct{}{} // 获取令牌 22 | go func() { 23 | defer func() { 24 | <-p.ch // 释放令牌 25 | p.wg.Done() 26 | }() 27 | task() 28 | }() 29 | } 30 | 31 | // Wait 等待所有任务完成 32 | func (p *GoroutinePool) Wait() { 33 | p.wg.Wait() 34 | } 35 | -------------------------------------------------------------------------------- /plugins/nosql/redis/conf/redis.proto: -------------------------------------------------------------------------------- 1 | // Defines the syntax version for the Protocol Buffers file. 2 | syntax = "proto3"; 3 | 4 | // Specifies the package name for the generated code. 5 | package lynx.protobuf.plugin.redis; 6 | 7 | // Sets the Go package name for the generated code. 8 | option go_package = "github.com/go-lynx/lynx/plugins/nosql/redis/conf;conf"; 9 | 10 | // Imports the Duration message type from the google/protobuf/duration.proto file. 11 | import "google/protobuf/duration.proto"; 12 | 13 | // Defines a message type for Redis configuration. 14 | message redis { 15 | // The network type (e.g., "tcp", "udp"). 16 | string network = 1; 17 | // The address of the Redis server. 18 | string addr = 2; 19 | // The password for the Redis server. 20 | string password = 3; 21 | // The database number to use. 22 | int32 db = 4; 23 | // The minimum number of idle connections to maintain. 24 | int32 min_idle_conns = 5; 25 | // The maximum number of idle connections to maintain. 26 | int32 max_idle_conns = 6; 27 | // The maximum number of active connections to allow. 28 | int32 max_active_conns = 7; 29 | // The maximum idle time for a connection. 30 | google.protobuf.Duration conn_max_idle_time = 8; 31 | // The timeout for establishing a connection. 32 | google.protobuf.Duration dial_timeout = 9; 33 | // The timeout for reading data from a connection. 34 | google.protobuf.Duration read_timeout = 10; 35 | // The timeout for writing data to a connection. 36 | google.protobuf.Duration write_timeout = 11; 37 | } 38 | -------------------------------------------------------------------------------- /plugins/nosql/redis/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/nosql/redis 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-kratos/kratos/v2 v2.8.4 // indirect 7 | github.com/go-lynx/lynx v1.2.1 8 | github.com/mattn/go-colorable v0.1.13 // indirect 9 | github.com/mattn/go-isatty v0.0.19 // indirect 10 | github.com/redis/go-redis/v9 v9.8.0 11 | github.com/rs/zerolog v1.34.0 // indirect 12 | google.golang.org/protobuf v1.35.2 13 | ) 14 | 15 | require ( 16 | dario.cat/mergo v1.0.0 // indirect 17 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 18 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 19 | github.com/go-kratos/aegis v0.2.0 // indirect 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/go-playground/form/v4 v4.2.1 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/gorilla/mux v1.8.1 // indirect 25 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 26 | go.opentelemetry.io/otel v1.33.0 // indirect 27 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 28 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 29 | golang.org/x/sync v0.13.0 // indirect 30 | golang.org/x/sys v0.32.0 // indirect 31 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 32 | google.golang.org/grpc v1.68.1 // indirect 33 | gopkg.in/yaml.v3 v3.0.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /plugins/nosql/redis/plug.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/go-lynx/lynx/app" 5 | "github.com/go-lynx/lynx/app/factory" 6 | "github.com/go-lynx/lynx/plugins" 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | // init 函数是 Go 语言的特殊函数,在包被加载时自动执行。 11 | // 此函数的作用是将 Redis 客户端插件注册到全局插件工厂中。 12 | func init() { 13 | // 调用全局插件工厂的 RegisterPlugin 方法进行插件注册。 14 | // 第一个参数 pluginName 为插件的唯一名称,用于标识该插件。 15 | // 第二个参数 confPrefix 是配置前缀,用于从配置中读取该插件相关的配置信息。 16 | // 第三个参数是一个匿名函数,该函数返回一个 plugins.Plugin 接口类型的实例, 17 | // 通过调用 NewRedisClient 函数创建一个新的 Redis 客户端插件实例。 18 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 19 | return NewRedisClient() 20 | }) 21 | } 22 | 23 | // GetRedis 函数用于获取 Redis 客户端实例。 24 | // 它通过全局 Lynx 应用实例获取插件管理器,再根据插件名称获取对应的插件实例, 25 | // 最后将插件实例转换为 *PlugRedis 类型并返回其 rdb 字段,即 Redis 客户端。 26 | func GetRedis() *redis.Client { 27 | plugin := app.Lynx().GetPluginManager().GetPlugin(pluginName) 28 | if plugin == nil { 29 | return nil 30 | } 31 | return plugin.(*PlugRedis).rdb 32 | } 33 | -------------------------------------------------------------------------------- /plugins/polaris/base.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | // GetNamespace 方法用于获取 PlugPolaris 实例对应的命名空间。 4 | // 命名空间通常用于在 Polaris 中隔离不同环境或业务的配置和服务。 5 | // 该方法通过调用 GetPlugin 函数获取 PlugPolaris 插件实例, 6 | // 然后从该实例的配置中提取命名空间信息。 7 | // 返回值为字符串类型,表示获取到的命名空间。 8 | func (p *PlugPolaris) GetNamespace() string { 9 | // 调用 GetPlugin 函数获取 PlugPolaris 插件实例, 10 | // 并从该实例的配置中调用 GetNamespace 方法获取命名空间 11 | return GetPlugin().conf.GetNamespace() 12 | } 13 | -------------------------------------------------------------------------------- /plugins/polaris/conf/polaris.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.polaris; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/polaris/conf;conf"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // Polaris 消息定义了 Polaris 服务的配置信息。 10 | // Polaris 是一个云原生服务发现和治理中心,此消息用于配置与 Polaris 交互所需的参数。 11 | message Polaris { 12 | // namespace 表示 Polaris 中的命名空间。 13 | // 命名空间用于隔离不同环境或业务的服务和配置,每个服务和配置都属于一个特定的命名空间。 14 | string namespace = 1; 15 | // token 是用于访问 Polaris 服务的认证令牌。 16 | // 该令牌用于验证客户端的身份,确保只有授权的客户端可以访问 Polaris 的服务和配置。 17 | string token = 2; 18 | // weight 表示服务实例的权重。 19 | // 在负载均衡时,权重会影响流量分配的比例,权重越高,分配到的流量可能越多。 20 | int32 weight = 4; 21 | // ttl 是服务实例的生存时间(Time To Live),单位为秒。 22 | // Polaris 会根据该值定期检查服务实例的健康状态,若超过该时间未收到心跳,实例可能会被标记为不健康。 23 | int32 ttl = 5; 24 | // timeout 是与 Polaris 服务交互时的超时时间。 25 | // 当发起请求到 Polaris 服务后,如果在该时间内未收到响应,则认为请求超时。 26 | google.protobuf.Duration timeout = 6; 27 | } 28 | -------------------------------------------------------------------------------- /plugins/polaris/config.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/contrib/polaris/v2" 5 | "github.com/go-kratos/kratos/v2/config" 6 | ) 7 | 8 | // GetConfig 方法用于从 Polaris 配置中心获取配置。 9 | // 该方法会根据传入的配置文件名和配置文件组名,从 Polaris 配置中心获取对应的配置源。 10 | // 参数 fileName 为要获取的配置文件的名称。 11 | // 参数 group 为配置文件所在的组名。 12 | // 返回值 config.Source 表示获取到的配置源,可用于后续的配置加载操作。 13 | // 返回值 error 表示获取配置过程中可能出现的错误,若操作成功则为 nil。 14 | func (p *PlugPolaris) GetConfig(fileName string, group string) (config.Source, error) { 15 | // 调用 GetPolaris() 函数获取 Polaris 实例, 16 | // 并使用该实例的 Config 方法结合 WithConfigFile 选项来设置要获取的配置文件信息。 17 | return GetPolaris().Config( 18 | // 使用 WithConfigFile 选项设置要获取的配置文件的详细信息 19 | polaris.WithConfigFile( 20 | polaris.File{ 21 | // 设置要获取的配置文件的名称 22 | Name: fileName, 23 | // 设置配置文件所属的组名 24 | Group: group, 25 | })) 26 | } 27 | -------------------------------------------------------------------------------- /plugins/polaris/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/polaris 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-kratos/kratos/contrib/polaris/v2 v2.0.0-20250429074618-c82f7957223f 7 | github.com/go-kratos/kratos/v2 v2.8.4 8 | github.com/go-lynx/lynx v1.2.1 9 | github.com/polarismesh/polaris-go v1.3.0 10 | google.golang.org/protobuf v1.35.2 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/dlclark/regexp2 v1.7.0 // indirect 18 | github.com/go-kratos/aegis v0.2.0 // indirect 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/go-logr/stdr v1.2.2 // indirect 21 | github.com/go-playground/form/v4 v4.2.0 // indirect 22 | github.com/golang/protobuf v1.5.4 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/gorilla/mux v1.8.1 // indirect 25 | github.com/hashicorp/errwrap v1.0.0 // indirect 26 | github.com/hashicorp/go-multierror v1.1.1 // indirect 27 | github.com/mattn/go-colorable v0.1.13 // indirect 28 | github.com/mattn/go-isatty v0.0.19 // indirect 29 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 30 | github.com/mitchellh/go-homedir v1.1.0 // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/natefinch/lumberjack v2.0.0+incompatible // indirect 33 | github.com/prometheus/client_golang v1.12.2 // indirect 34 | github.com/prometheus/client_model v0.2.0 // indirect 35 | github.com/prometheus/common v0.32.1 // indirect 36 | github.com/prometheus/procfs v0.7.3 // indirect 37 | github.com/rs/zerolog v1.34.0 // indirect 38 | github.com/spaolacci/murmur3 v1.1.0 // indirect 39 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 40 | go.opentelemetry.io/otel v1.33.0 // indirect 41 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 42 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 43 | go.uber.org/atomic v1.9.0 // indirect 44 | go.uber.org/multierr v1.7.0 // indirect 45 | go.uber.org/zap v1.21.0 // indirect 46 | golang.org/x/net v0.39.0 // indirect 47 | golang.org/x/sync v0.13.0 // indirect 48 | golang.org/x/sys v0.32.0 // indirect 49 | golang.org/x/text v0.24.0 // indirect 50 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 51 | google.golang.org/grpc v1.68.1 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /plugins/polaris/limiter.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/contrib/polaris/v2" 5 | "github.com/go-kratos/kratos/v2/middleware" 6 | "github.com/go-lynx/lynx/app" 7 | "github.com/go-lynx/lynx/app/log" 8 | ) 9 | 10 | // HTTPRateLimit 方法用于创建一个 HTTP 限流中间件。 11 | // 该中间件会从 Polaris 获取 HTTP 限流策略,并应用到 HTTP 请求处理流程中。 12 | // 返回值为一个实现了 middleware.Middleware 接口的中间件实例。 13 | func (p *PlugPolaris) HTTPRateLimit() middleware.Middleware { 14 | // 使用 Lynx 应用的日志辅助器记录正在同步 HTTP 限流策略的信息 15 | log.Infof("Synchronizing [HTTP] rate limit policy") 16 | // 调用 GetPolaris().Limiter 方法获取一个限流实例,同时设置服务名称和命名空间 17 | // 服务名称通过 app.GetName() 获取,命名空间从插件配置中获取 18 | // 最后调用 polaris.RateLimit 方法将限流实例转换为中间件 19 | return polaris.Ratelimit(GetPolaris().Limiter( 20 | // 设置限流服务名称为当前应用的名称 21 | polaris.WithLimiterService(app.GetName()), 22 | // 设置限流服务的命名空间为插件配置中的命名空间 23 | polaris.WithLimiterNamespace(GetPlugin().conf.Namespace), 24 | )) 25 | } 26 | 27 | // GRPCRateLimit 方法用于创建一个 gRPC 限流中间件。 28 | // 该中间件会从 Polaris 获取 gRPC 限流策略,并应用到 gRPC 请求处理流程中。 29 | // 返回值为一个实现了 middleware.Middleware 接口的中间件实例。 30 | func (p *PlugPolaris) GRPCRateLimit() middleware.Middleware { 31 | // 使用 Lynx 应用的日志辅助器记录正在同步 gRPC 限流策略的信息 32 | log.Infof("Synchronizing [GRPC] rate limit policy") 33 | // 调用 GetPolaris().Limiter 方法获取一个限流实例,同时设置服务名称和命名空间 34 | // 服务名称通过 app.GetName() 获取,命名空间从插件配置中获取 35 | // 最后调用 polaris.RateLimit 方法将限流实例转换为中间件 36 | return polaris.Ratelimit(GetPolaris().Limiter( 37 | // 设置限流服务名称为当前应用的名称 38 | polaris.WithLimiterService(app.GetName()), 39 | // 设置限流服务的命名空间为插件配置中的命名空间 40 | polaris.WithLimiterNamespace(GetPlugin().conf.Namespace), 41 | )) 42 | } 43 | -------------------------------------------------------------------------------- /plugins/polaris/plug.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/contrib/polaris/v2" 5 | "github.com/go-lynx/lynx/app" 6 | "github.com/go-lynx/lynx/app/factory" 7 | "github.com/go-lynx/lynx/plugins" 8 | ) 9 | 10 | // init 函数会在包被导入时自动执行,用于将 Polaris 插件注册到全局插件工厂。 11 | // 注册后,插件管理器可以发现并加载该插件。 12 | func init() { 13 | // 从全局插件工厂获取实例,调用其 RegisterPlugin 方法进行插件注册。 14 | // 参数 name 为插件的名称,用于唯一标识插件。 15 | // 参数 confPrefix 是插件配置的前缀,用于从配置文件中加载插件相关配置。 16 | // 最后一个参数是一个函数,返回一个实现了 plugins.Plugin 接口的实例。 17 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 18 | // 调用 Polaris 函数获取插件实例并返回 19 | return NewPolarisControlPlane() 20 | }) 21 | } 22 | 23 | // GetPolaris 函数用于从应用的插件管理器中获取 Polaris 实例。 24 | // 该实例可用于与 Polaris 服务进行交互,如服务发现、配置管理等。 25 | // 返回值为 *polaris.Polaris 类型的指针,指向获取到的 Polaris 实例。 26 | func GetPolaris() *polaris.Polaris { 27 | // 从应用的插件管理器中获取指定名称的插件实例, 28 | // 并将其类型断言为 *PlugPolaris,然后返回其内部的 polaris 字段。 29 | return app.Lynx().GetPluginManager().GetPlugin(pluginName).(*PlugPolaris).polaris 30 | } 31 | 32 | // GetPlugin 函数用于从应用的插件管理器中获取 PlugPolaris 插件实例。 33 | // 该实例可用于调用插件提供的各种方法。 34 | // 返回值为 *PlugPolaris 类型的指针,指向获取到的插件实例。 35 | func GetPlugin() *PlugPolaris { 36 | // 从应用的插件管理器中获取指定名称的插件实例, 37 | // 并将其类型断言为 *PlugPolaris 后返回。 38 | plugin := app.Lynx().GetPluginManager().GetPlugin(pluginName) 39 | if plugin == nil { 40 | return nil 41 | } 42 | return plugin.(*PlugPolaris) 43 | } 44 | -------------------------------------------------------------------------------- /plugins/polaris/polaris.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/contrib/polaris/v2" 5 | "github.com/go-lynx/lynx/app" 6 | "github.com/go-lynx/lynx/app/log" 7 | "github.com/go-lynx/lynx/plugins" 8 | "github.com/go-lynx/lynx/plugins/polaris/conf" 9 | "github.com/polarismesh/polaris-go/api" 10 | "google.golang.org/protobuf/types/known/durationpb" 11 | "math" 12 | ) 13 | 14 | // Plugin metadata 15 | // 插件元数据,定义插件的基本信息 16 | const ( 17 | // pluginName 是 HTTP 服务器插件的唯一标识符,用于在插件系统中识别该插件。 18 | pluginName = "polaris.control.plane" 19 | 20 | // pluginVersion 表示 HTTP 服务器插件的当前版本。 21 | pluginVersion = "v2.0.0" 22 | 23 | // pluginDescription 简要描述了 HTTP 服务器插件的功能。 24 | pluginDescription = "polaris control plane plugin for lynx framework" 25 | 26 | // confPrefix 是加载 HTTP 服务器配置时使用的配置前缀。 27 | confPrefix = "lynx.polaris" 28 | ) 29 | 30 | type PlugPolaris struct { 31 | *plugins.BasePlugin 32 | polaris *polaris.Polaris 33 | conf *conf.Polaris 34 | } 35 | 36 | // NewPolarisControlPlane 创建一个新的 控制平面 Polaris。 37 | // 该函数初始化插件的基础信息,并返回一个指向 PolarisControlPlane 的指针。 38 | func NewPolarisControlPlane() *PlugPolaris { 39 | return &PlugPolaris{ 40 | BasePlugin: plugins.NewBasePlugin( 41 | // 生成插件的唯一 ID 42 | plugins.GeneratePluginID("", pluginName, pluginVersion), 43 | // 插件名称 44 | pluginName, 45 | // 插件描述 46 | pluginDescription, 47 | // 插件版本 48 | pluginVersion, 49 | // 配置前缀 50 | confPrefix, 51 | // 权重 52 | math.MaxInt, 53 | ), 54 | } 55 | } 56 | 57 | // InitializeResources 实现了 Polaris 插件的自定义初始化逻辑。 58 | // 该函数会加载并验证 Polaris 配置,如果配置未提供,则使用默认配置。 59 | func (p *PlugPolaris) InitializeResources(rt plugins.Runtime) error { 60 | // 初始化一个空的配置结构 61 | p.conf = &conf.Polaris{} 62 | 63 | // 从运行时配置中扫描并加载 Polaris 配置 64 | err := rt.GetConfig().Value(confPrefix).Scan(p.conf) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // 设置默认配置 70 | defaultConf := &conf.Polaris{ 71 | // 默认命名空间为 default 72 | Namespace: "default", 73 | // 默认服务实例权重为 100 74 | Weight: 100, 75 | // 默认 TTL 为 5 秒 76 | Ttl: 5, 77 | // 默认超时时间为 5 秒 78 | Timeout: &durationpb.Duration{Seconds: 5}, 79 | } 80 | 81 | // 对未设置的字段使用默认值 82 | if p.conf.Namespace == "" { 83 | p.conf.Namespace = defaultConf.Namespace 84 | } 85 | if p.conf.Weight == 0 { 86 | p.conf.Weight = defaultConf.Weight 87 | } 88 | if p.conf.Ttl == 0 { 89 | p.conf.Ttl = defaultConf.Ttl 90 | } 91 | if p.conf.Timeout == nil { 92 | p.conf.Timeout = defaultConf.Timeout 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // StartupTasks 实现了 HTTP 插件的自定义启动逻辑。 99 | // 该函数会配置并启动 HTTP 服务器,添加必要的中间件和配置选项。 100 | func (p *PlugPolaris) StartupTasks() error { 101 | // 使用 Lynx 应用的 Helper 记录 Polaris 插件初始化的信息。 102 | log.Infof("Initializing polaris plugin") 103 | 104 | // 初始化 Polaris SDK 上下文。 105 | sdk, err := api.InitContextByConfig(api.NewConfiguration()) 106 | // 如果初始化失败,记录错误信息并抛出 panic。 107 | if err != nil { 108 | log.Error(err) 109 | panic(err) 110 | } 111 | 112 | // 创建一个新的 Polaris 实例,使用之前初始化的 SDK 和配置。 113 | polar := polaris.New( 114 | sdk, 115 | polaris.WithService(app.GetName()), 116 | polaris.WithNamespace(p.conf.Namespace), 117 | ) 118 | // 将 Polaris 实例保存到 p.polaris 中。 119 | p.polaris = &polar 120 | 121 | // 设置 Polaris 控制平面为 Lynx 应用的控制平面。 122 | err = app.Lynx().SetControlPlane(p) 123 | if err != nil { 124 | log.Error(err) 125 | return err 126 | } 127 | 128 | // 获取 Lynx 应用的控制平面启动配置。 129 | cfg, err := app.Lynx().InitControlPlaneConfig() 130 | if err != nil { 131 | log.Error(err) 132 | return err 133 | } 134 | 135 | // 加载插件列表中的插件。 136 | app.Lynx().GetPluginManager().LoadPlugins(cfg) 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /plugins/polaris/registry.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/contrib/polaris/v2" 5 | "github.com/go-kratos/kratos/v2/registry" 6 | "github.com/go-lynx/lynx/app/log" 7 | ) 8 | 9 | // NewServiceRegistry 方法用于创建一个新的 Polaris 服务注册器 10 | // The NewServiceRegistry method is used to create a new Polaris service registrar. 11 | // 该方法会记录服务注册的日志信息,并根据配置初始化 Polaris 注册器 12 | // This method logs service registration information and initializes the Polaris registrar based on the configuration. 13 | func (p *PlugPolaris) NewServiceRegistry() registry.Registrar { 14 | // 使用 Lynx 应用的日志工具记录服务正在注册的信息 15 | // Use the Lynx application's logging tool to record that the service is being registered. 16 | log.Infof("Service registration in progress") 17 | // 调用 GetPolaris() 函数获取 Polaris 实例,并通过一系列选项配置注册器 18 | // Call the GetPolaris() function to obtain a Polaris instance and configure the registrar with a series of options. 19 | reg := GetPolaris().Registry( 20 | // 使用 WithRegistryServiceToken 方法设置服务令牌 21 | // Use the WithRegistryServiceToken method to set the service token. 22 | polaris.WithRegistryServiceToken(GetPlugin().conf.Token), 23 | // 使用 WithRegistryTimeout 方法设置注册超时时间 24 | // Use the WithRegistryTimeout method to set the registration timeout. 25 | polaris.WithRegistryTimeout(GetPlugin().conf.Timeout.AsDuration()), 26 | // 使用 WithRegistryTTL 方法设置注册的 TTL(生存时间) 27 | // Use the WithRegistryTTL method to set the registration TTL (Time To Live). 28 | polaris.WithRegistryTTL(int(GetPlugin().conf.Ttl)), 29 | // 使用 WithRegistryWeight 方法设置注册的权重 30 | // Use the WithRegistryWeight method to set the registration weight. 31 | polaris.WithRegistryWeight(int(GetPlugin().conf.Weight)), 32 | ) 33 | // 返回创建好的服务注册器实例 34 | // Return the created service registrar instance. 35 | return reg 36 | } 37 | 38 | // NewServiceDiscovery 方法用于创建一个新的 Polaris 服务发现器 39 | // The NewServiceDiscovery method is used to create a new Polaris service discoverer. 40 | // 该方法会记录服务发现的日志信息,并根据配置初始化 Polaris 发现器 41 | // This method logs service discovery information and initializes the Polaris discoverer based on the configuration. 42 | func (p *PlugPolaris) NewServiceDiscovery() registry.Discovery { 43 | // 使用 Lynx 应用的日志工具记录服务正在进行发现的信息 44 | // Use the Lynx application's logging tool to record that the service discovery is in progress. 45 | log.Infof("Service discovery in progress") 46 | // 调用 GetPolaris() 函数获取 Polaris 实例,并通过一系列选项配置发现器 47 | // Call the GetPolaris() function to obtain a Polaris instance and configure the discoverer with a series of options. 48 | reg := GetPolaris().Registry( 49 | // 使用 WithRegistryServiceToken 方法设置服务令牌 50 | // Use the WithRegistryServiceToken method to set the service token. 51 | polaris.WithRegistryServiceToken(GetPlugin().conf.Token), 52 | // 使用 WithRegistryTimeout 方法设置发现超时时间 53 | // Use the WithRegistryTimeout method to set the discovery timeout. 54 | polaris.WithRegistryTimeout(GetPlugin().conf.Timeout.AsDuration()), 55 | // 使用 WithRegistryTTL 方法设置发现的 TTL(生存时间) 56 | // Use the WithRegistryTTL method to set the discovery TTL (Time To Live). 57 | polaris.WithRegistryTTL(int(GetPlugin().conf.Ttl)), 58 | // 使用 WithRegistryWeight 方法设置发现的权重 59 | // Use the WithRegistryWeight method to set the discovery weight. 60 | polaris.WithRegistryWeight(int(GetPlugin().conf.Weight)), 61 | ) 62 | // 返回创建好的服务发现器实例 63 | // Return the created service discoverer instance. 64 | return reg 65 | } 66 | -------------------------------------------------------------------------------- /plugins/polaris/router.go: -------------------------------------------------------------------------------- 1 | package polaris 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/contrib/polaris/v2" 5 | "github.com/go-kratos/kratos/v2/selector" 6 | "github.com/go-lynx/lynx/app/log" 7 | ) 8 | 9 | // NewNodeRouter method is used to create a new Polaris node filter for synchronizing the routing policies of remote services 10 | func (p *PlugPolaris) NewNodeRouter(name string) selector.NodeFilter { 11 | // Use the Lynx application's Helper to record information about the routing policy for the specified name being synchronized 12 | log.Infof("Synchronizing [%v] routing policy", name) 13 | // Call the GetPolaris().NodeFilter method to obtain a node filter instance and set the service name 14 | return GetPolaris().NodeFilter(polaris.WithRouterService(name)) 15 | } 16 | -------------------------------------------------------------------------------- /plugins/seata/conf/seata.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.36.6 4 | // protoc v4.23.0 5 | // source: seata.proto 6 | 7 | package conf 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | unsafe "unsafe" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // Seata 消息定义了 Seata 插件的配置信息。 25 | type Seata struct { 26 | state protoimpl.MessageState `protogen:"open.v1"` 27 | // enabled 表示是否启用 Seata 插件。 28 | // 设置为 true 时启用插件,设置为 false 时禁用插件。 29 | Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` 30 | // config_file_path 表示 Seata 配置文件的路径。 31 | // 该路径指向包含 Seata 客户端所需配置信息的文件。 32 | ConfigFilePath string `protobuf:"bytes,2,opt,name=config_file_path,json=configFilePath,proto3" json:"config_file_path,omitempty"` 33 | unknownFields protoimpl.UnknownFields 34 | sizeCache protoimpl.SizeCache 35 | } 36 | 37 | func (x *Seata) Reset() { 38 | *x = Seata{} 39 | mi := &file_seata_proto_msgTypes[0] 40 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 41 | ms.StoreMessageInfo(mi) 42 | } 43 | 44 | func (x *Seata) String() string { 45 | return protoimpl.X.MessageStringOf(x) 46 | } 47 | 48 | func (*Seata) ProtoMessage() {} 49 | 50 | func (x *Seata) ProtoReflect() protoreflect.Message { 51 | mi := &file_seata_proto_msgTypes[0] 52 | if x != nil { 53 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 54 | if ms.LoadMessageInfo() == nil { 55 | ms.StoreMessageInfo(mi) 56 | } 57 | return ms 58 | } 59 | return mi.MessageOf(x) 60 | } 61 | 62 | // Deprecated: Use Seata.ProtoReflect.Descriptor instead. 63 | func (*Seata) Descriptor() ([]byte, []int) { 64 | return file_seata_proto_rawDescGZIP(), []int{0} 65 | } 66 | 67 | func (x *Seata) GetEnabled() bool { 68 | if x != nil { 69 | return x.Enabled 70 | } 71 | return false 72 | } 73 | 74 | func (x *Seata) GetConfigFilePath() string { 75 | if x != nil { 76 | return x.ConfigFilePath 77 | } 78 | return "" 79 | } 80 | 81 | var File_seata_proto protoreflect.FileDescriptor 82 | 83 | const file_seata_proto_rawDesc = "" + 84 | "\n" + 85 | "\vseata.proto\x12\x1alynx.protobuf.plugin.seata\"K\n" + 86 | "\x05Seata\x12\x18\n" + 87 | "\aenabled\x18\x01 \x01(\bR\aenabled\x12(\n" + 88 | "\x10config_file_path\x18\x02 \x01(\tR\x0econfigFilePathB1Z/github.com/go-lynx/lynx/plugins/seata/conf;confb\x06proto3" 89 | 90 | var ( 91 | file_seata_proto_rawDescOnce sync.Once 92 | file_seata_proto_rawDescData []byte 93 | ) 94 | 95 | func file_seata_proto_rawDescGZIP() []byte { 96 | file_seata_proto_rawDescOnce.Do(func() { 97 | file_seata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_seata_proto_rawDesc), len(file_seata_proto_rawDesc))) 98 | }) 99 | return file_seata_proto_rawDescData 100 | } 101 | 102 | var file_seata_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 103 | var file_seata_proto_goTypes = []any{ 104 | (*Seata)(nil), // 0: lynx.protobuf.plugin.seata.Seata 105 | } 106 | var file_seata_proto_depIdxs = []int32{ 107 | 0, // [0:0] is the sub-list for method output_type 108 | 0, // [0:0] is the sub-list for method input_type 109 | 0, // [0:0] is the sub-list for extension type_name 110 | 0, // [0:0] is the sub-list for extension extendee 111 | 0, // [0:0] is the sub-list for field type_name 112 | } 113 | 114 | func init() { file_seata_proto_init() } 115 | func file_seata_proto_init() { 116 | if File_seata_proto != nil { 117 | return 118 | } 119 | type x struct{} 120 | out := protoimpl.TypeBuilder{ 121 | File: protoimpl.DescBuilder{ 122 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 123 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_seata_proto_rawDesc), len(file_seata_proto_rawDesc)), 124 | NumEnums: 0, 125 | NumMessages: 1, 126 | NumExtensions: 0, 127 | NumServices: 0, 128 | }, 129 | GoTypes: file_seata_proto_goTypes, 130 | DependencyIndexes: file_seata_proto_depIdxs, 131 | MessageInfos: file_seata_proto_msgTypes, 132 | }.Build() 133 | File_seata_proto = out.File 134 | file_seata_proto_goTypes = nil 135 | file_seata_proto_depIdxs = nil 136 | } 137 | -------------------------------------------------------------------------------- /plugins/seata/conf/seata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.seata; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/seata/conf;conf"; 6 | 7 | // Seata 消息定义了 Seata 插件的配置信息。 8 | message Seata { 9 | // enabled 表示是否启用 Seata 插件。 10 | // 设置为 true 时启用插件,设置为 false 时禁用插件。 11 | bool enabled = 1; 12 | // config_file_path 表示 Seata 配置文件的路径。 13 | // 该路径指向包含 Seata 客户端所需配置信息的文件。 14 | string config_file_path = 2; 15 | } 16 | -------------------------------------------------------------------------------- /plugins/seata/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/seata 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-lynx/lynx v1.2.1 7 | github.com/seata/seata-go v1.2.0 8 | google.golang.org/protobuf v1.36.6 9 | ) 10 | 11 | require ( 12 | dario.cat/mergo v1.0.0 // indirect 13 | dubbo.apache.org/dubbo-go/v3 v3.0.3-rc2 // indirect 14 | github.com/RoaringBitmap/roaring v1.2.0 // indirect 15 | github.com/Workiva/go-datastructures v1.0.52 // indirect 16 | github.com/apache/dubbo-getty v1.4.9-0.20220825024508-3da63c3257fa // indirect 17 | github.com/apache/dubbo-go-hessian2 v1.11.1 // indirect 18 | github.com/arana-db/parser v0.2.5 // indirect 19 | github.com/benbjohnson/clock v1.1.0 // indirect 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/bits-and-blooms/bitset v1.2.0 // indirect 22 | github.com/bluele/gcache v0.0.2 // indirect 23 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 | github.com/creasty/defaults v1.5.2 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/dsnet/compress v0.0.1 // indirect 27 | github.com/dubbogo/gost v1.12.6-0.20220824084206-300e27e9e524 // indirect 28 | github.com/go-kratos/aegis v0.2.0 // indirect 29 | github.com/go-kratos/kratos/v2 v2.8.4 // indirect 30 | github.com/go-logr/logr v1.4.2 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/go-ole/go-ole v1.2.6 // indirect 33 | github.com/go-playground/form/v4 v4.2.0 // indirect 34 | github.com/go-sql-driver/mysql v1.6.0 // indirect 35 | github.com/goccy/go-json v0.9.7 // indirect 36 | github.com/golang/protobuf v1.5.4 // indirect 37 | github.com/golang/snappy v0.0.4 // indirect 38 | github.com/google/uuid v1.6.0 // indirect 39 | github.com/gorilla/mux v1.8.1 // indirect 40 | github.com/gorilla/websocket v1.4.2 // indirect 41 | github.com/jinzhu/copier v0.3.5 // indirect 42 | github.com/k0kubun/pp v3.0.1+incompatible // indirect 43 | github.com/klauspost/compress v1.15.11 // indirect 44 | github.com/knadh/koanf v1.4.3 // indirect 45 | github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect 46 | github.com/magiconair/properties v1.8.6 // indirect 47 | github.com/mattn/go-colorable v0.1.13 // indirect 48 | github.com/mattn/go-isatty v0.0.19 // indirect 49 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 50 | github.com/mitchellh/copystructure v1.2.0 // indirect 51 | github.com/mitchellh/mapstructure v1.5.0 // indirect 52 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 53 | github.com/mschoch/smat v0.2.0 // indirect 54 | github.com/natefinch/lumberjack v2.0.0+incompatible // indirect 55 | github.com/pelletier/go-toml v1.9.3 // indirect 56 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 57 | github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect 58 | github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 // indirect 59 | github.com/pkg/errors v0.9.1 // indirect 60 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect 61 | github.com/prometheus/client_golang v1.12.2 // indirect 62 | github.com/prometheus/client_model v0.2.0 // indirect 63 | github.com/prometheus/common v0.32.1 // indirect 64 | github.com/prometheus/procfs v0.7.3 // indirect 65 | github.com/rs/zerolog v1.34.0 // indirect 66 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect 67 | github.com/shirou/gopsutil/v3 v3.23.6 // indirect 68 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 69 | github.com/sijms/go-ora/v2 v2.5.17 // indirect 70 | github.com/tklauser/go-sysconf v0.3.11 // indirect 71 | github.com/tklauser/numcpus v0.6.1 // indirect 72 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 73 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 74 | go.opentelemetry.io/otel v1.33.0 // indirect 75 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 76 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 77 | go.uber.org/atomic v1.9.0 // indirect 78 | go.uber.org/multierr v1.7.0 // indirect 79 | go.uber.org/zap v1.21.0 // indirect 80 | golang.org/x/sync v0.13.0 // indirect 81 | golang.org/x/sys v0.32.0 // indirect 82 | golang.org/x/text v0.24.0 // indirect 83 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 84 | google.golang.org/grpc v1.68.1 // indirect 85 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 86 | gopkg.in/yaml.v2 v2.4.0 // indirect 87 | gopkg.in/yaml.v3 v3.0.1 // indirect 88 | vimagination.zapto.org/byteio v0.0.0-20200222190125-d27cba0f0b10 // indirect 89 | ) 90 | -------------------------------------------------------------------------------- /plugins/seata/plug.go: -------------------------------------------------------------------------------- 1 | package seata 2 | 3 | import ( 4 | "github.com/go-lynx/lynx/app/factory" 5 | "github.com/go-lynx/lynx/plugins" 6 | ) 7 | 8 | // init 是 Go 语言的初始化函数,在包被加载时自动执行。 9 | // 此函数的作用是将 Seata 客户端插件注册到全局插件工厂中。 10 | func init() { 11 | // 调用全局插件工厂实例的 RegisterPlugin 方法进行插件注册。 12 | // 第一个参数 pluginName 是插件的唯一名称,用于在系统中标识该插件。 13 | // 第二个参数 confPrefix 是配置前缀,用于从配置文件中读取该插件的相关配置。 14 | // 第三个参数是一个匿名函数,该函数返回一个实现了 plugins.Plugin 接口的实例。 15 | // 通过调用 NewSeataClient 函数创建一个新的 Seata 客户端插件实例并返回。 16 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 17 | return NewTxSeataClient() 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /plugins/seata/seata.go: -------------------------------------------------------------------------------- 1 | package seata 2 | 3 | import ( 4 | "github.com/go-lynx/lynx/app/log" 5 | "github.com/go-lynx/lynx/plugins" 6 | "github.com/go-lynx/lynx/plugins/seata/conf" 7 | "github.com/seata/seata-go/pkg/client" 8 | ) 9 | 10 | // Plugin metadata 11 | // 插件元数据,定义插件的基本信息 12 | const ( 13 | // pluginName 是 HTTP 服务器插件的唯一标识符,用于在插件系统中识别该插件。 14 | pluginName = "seata.server" 15 | 16 | // pluginVersion 表示 HTTP 服务器插件的当前版本。 17 | pluginVersion = "v2.0.0" 18 | 19 | // pluginDescription 简要描述了 HTTP 服务器插件的功能。 20 | pluginDescription = "seata transaction server plugin for Lynx framework" 21 | 22 | // confPrefix 是加载 HTTP 服务器配置时使用的配置前缀。 23 | confPrefix = "lynx.seata" 24 | ) 25 | 26 | type TxSeataClient struct { 27 | // 嵌入基础插件,继承插件的通用属性和方法 28 | *plugins.BasePlugin 29 | // HTTP 服务器的配置信息 30 | conf *conf.Seata 31 | } 32 | 33 | // NewTxSeataClient 创建一个新的 HTTP 服务器插件实例。 34 | // 该函数初始化插件的基础信息,并返回一个指向 ServiceHttp 结构体的指针。 35 | func NewTxSeataClient() *TxSeataClient { 36 | return &TxSeataClient{ 37 | BasePlugin: plugins.NewBasePlugin( 38 | // 生成插件的唯一 ID 39 | plugins.GeneratePluginID("", pluginName, pluginVersion), 40 | // 插件名称 41 | pluginName, 42 | // 插件描述 43 | pluginDescription, 44 | // 插件版本 45 | pluginVersion, 46 | // 配置前缀 47 | confPrefix, 48 | // 权重 49 | 90, 50 | ), 51 | } 52 | } 53 | 54 | // InitializeResources 方法用于加载并初始化 Seata 插件 55 | func (t *TxSeataClient) InitializeResources(rt plugins.Runtime) error { 56 | // 初始化一个空的配置结构 57 | t.conf = &conf.Seata{} 58 | 59 | // 从运行时配置中扫描并加载 Seata 配置 60 | err := rt.GetConfig().Value(confPrefix).Scan(t.conf) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // 设置默认配置 66 | defaultConf := &conf.Seata{ 67 | // 默认配置文件路径为 ./conf/seata.yml 68 | ConfigFilePath: "./conf/seata.yml", 69 | } 70 | 71 | // 对未设置的字段使用默认值 72 | if t.conf.ConfigFilePath == "" { 73 | t.conf.ConfigFilePath = defaultConf.ConfigFilePath 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (t *TxSeataClient) StartupTasks() error { 80 | // 使用 Lynx 应用的 Helper 记录 Seata 插件初始化的信息 81 | log.Infof("Initializing seata") 82 | // 如果 Seata 插件已启用,则初始化 Seata 客户端 83 | if t.conf.GetEnabled() { 84 | // 调用 client.InitPath 方法初始化 Seata 客户端,使用配置中的路径 85 | client.InitPath(t.conf.GetConfigFilePath()) 86 | } 87 | // 使用 Lynx 应用的 Helper 记录 Seata 服务初始化成功的信息 88 | log.Infof("seata successfully initialized") 89 | // 返回 Seata 插件实例和 nil 错误,表示加载成功 90 | return nil 91 | } 92 | 93 | func (t *TxSeataClient) CleanupTasks() error { 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /plugins/service/grpc/README.md: -------------------------------------------------------------------------------- 1 | # gRPC Plugin for Lynx Framework 2 | 3 | This plugin provides gRPC server functionality for the Lynx framework, offering features such as TLS support, middleware integration, and configuration management. 4 | 5 | ## Features 6 | 7 | - Full gRPC server implementation 8 | - TLS support with client authentication 9 | - Built-in middleware support: 10 | - Tracing (OpenTelemetry) 11 | - Logging 12 | - Rate limiting 13 | - Request validation 14 | - Panic recovery 15 | - Dynamic configuration 16 | - Health checking 17 | - Graceful shutdown 18 | 19 | ## Installation 20 | 21 | ```bash 22 | go get github.com/go-lynx/plugin-grpc/v2 23 | ``` 24 | 25 | ## Configuration 26 | 27 | The plugin can be configured through the Lynx configuration system. Here's an example configuration: 28 | 29 | ```yaml 30 | lynx: 31 | server: 32 | network: "tcp" 33 | addr: ":9090" 34 | timeout: "1s" 35 | tls: true 36 | tls_auth_type: 4 # Mutual TLS authentication 37 | ``` 38 | 39 | ### Configuration Options 40 | 41 | - `network`: Network type (default: "tcp") 42 | - `addr`: Server address (default: ":9090") 43 | - `timeout`: Request timeout duration 44 | - `tls`: Enable/disable TLS 45 | - `tls_auth_type`: TLS authentication type 46 | - 0: No client authentication 47 | - 1: Request client certificate 48 | - 2: Require any client certificate 49 | - 3: Verify client certificate if given 50 | - 4: Require and verify client certificate 51 | 52 | ## Usage 53 | 54 | ### Basic Usage 55 | 56 | ```go 57 | package main 58 | 59 | import ( 60 | "github.com/go-lynx/lynx/app" 61 | "github.com/go-lynx/plugin-grpc/v2" 62 | pb "your/protobuf/package" 63 | ) 64 | 65 | func main() { 66 | // Initialize your Lynx application 67 | application := app.NewApplication() 68 | 69 | // The gRPC plugin will be automatically registered and initialized 70 | 71 | // Get the gRPC server instance 72 | server := grpc.GetServer() 73 | 74 | // Register your gRPC service 75 | pb.RegisterYourServiceServer(server, &YourServiceImpl{}) 76 | 77 | // Start the application 78 | if err := application.Run(); err != nil { 79 | panic(err) 80 | } 81 | } 82 | ``` 83 | 84 | ### With TLS 85 | 86 | To use TLS, you need to: 87 | 88 | 1. Enable TLS in configuration 89 | 2. Provide certificates through the Lynx certificate management system 90 | 3. Configure client authentication type if needed 91 | 92 | ```go 93 | // Your certificates will be automatically loaded from the configuration 94 | // and applied to the gRPC server 95 | ``` 96 | 97 | ### Custom Middleware 98 | 99 | The plugin comes with several built-in middleware options. You can also add your own middleware: 100 | 101 | ```go 102 | package main 103 | 104 | import ( 105 | "context" 106 | "github.com/go-lynx/lynx/app" 107 | "github.com/go-lynx/plugin-grpc/v2" 108 | "google.golang.org/grpc" 109 | ) 110 | 111 | func main() { 112 | // Initialize your application 113 | application := app.NewApplication() 114 | 115 | // Get the gRPC server 116 | server := grpc.GetServer() 117 | 118 | // Add your custom middleware 119 | server.Use(YourCustomMiddleware()) 120 | 121 | // Start the application 122 | if err := application.Run(); err != nil { 123 | panic(err) 124 | } 125 | } 126 | 127 | func YourCustomMiddleware() grpc.UnaryServerInterceptor { 128 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 129 | // Your middleware logic here 130 | return handler(ctx, req) 131 | } 132 | } 133 | ``` 134 | 135 | ## Health Checking 136 | 137 | The plugin implements health checking through the Lynx plugin system. You can monitor the gRPC server's health status through your application's health checking mechanism. 138 | 139 | ## Dependencies 140 | 141 | - github.com/go-kratos/kratos/v2 142 | - github.com/go-lynx/lynx 143 | - google.golang.org/grpc 144 | 145 | ## License 146 | 147 | Apache License 2.0 148 | -------------------------------------------------------------------------------- /plugins/service/grpc/conf/grpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.grpc; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/service/grpc/conf;conf"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // grpc 消息定义了 gRPC 服务器插件的配置信息。 10 | // The grpc message defines the configuration information for the gRPC server plugin. 11 | message grpc { 12 | // Network 指定网络类型(例如 "tcp"、"unix"),用于确定 gRPC 服务器监听的网络协议。 13 | // Network specifies the network type (e.g., "tcp", "unix") for the gRPC server to listen on. 14 | string network = 1; 15 | 16 | // Addr 指定 gRPC 服务器监听的地址(例如 ":9090", "localhost:9090")。 17 | // Addr specifies the address for the gRPC server to listen on (e.g., ":9090", "localhost:9090"). 18 | string addr = 2; 19 | 20 | // Tls 指示是否启用 TLS/GRPCS 加密通信。 21 | // Tls indicates whether TLS/GRPCS encryption is enabled. 22 | bool tls_enable = 3; 23 | 24 | // TlsAuthType 指定 TLS 客户端认证类型,不同的值代表不同的认证策略: 25 | // 0: 不进行客户端认证 26 | // 1: 请求客户端证书,但不强制要求 27 | // 2: 强制要求客户端提供证书 28 | // 3: 验证客户端证书 29 | // 4: 若客户端提供证书,则进行验证 30 | // TlsAuthType specifies the TLS client authentication type. Different values represent different authentication strategies: 31 | // 0: No client authentication 32 | // 1: Request client certificate, but not mandatory 33 | // 2: Require client certificate 34 | // 3: Verify client certificate 35 | // 4: Verify client certificate if provided 36 | int32 tls_auth_type = 4; 37 | 38 | // Timeout 指定处理 gRPC 请求的最大时长,超过该时长请求可能会被终止。 39 | // Timeout specifies the maximum duration for handling gRPC requests. Requests may be terminated if they exceed this duration. 40 | google.protobuf.Duration timeout = 5; 41 | } 42 | -------------------------------------------------------------------------------- /plugins/service/grpc/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/service/grpc 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-kratos/kratos/contrib/middleware/validate/v2 v2.0.0-20250527152916-d6f5f00cf562 7 | github.com/go-kratos/kratos/v2 v2.8.4 8 | github.com/go-lynx/lynx v1.2.1 9 | google.golang.org/protobuf v1.36.5 10 | ) 11 | 12 | require ( 13 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 // indirect 14 | cel.dev/expr v0.22.0 // indirect 15 | dario.cat/mergo v1.0.0 // indirect 16 | github.com/antlr4-go/antlr/v4 v4.13.1 // indirect 17 | github.com/bufbuild/protovalidate-go v0.9.2 // indirect 18 | github.com/go-kratos/aegis v0.2.0 // indirect 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/go-logr/stdr v1.2.2 // indirect 21 | github.com/go-playground/form/v4 v4.2.1 // indirect 22 | github.com/google/cel-go v0.24.1 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/gorilla/mux v1.8.1 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.19 // indirect 27 | github.com/rs/zerolog v1.34.0 // indirect 28 | github.com/stoewer/go-strcase v1.3.0 // indirect 29 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 30 | go.opentelemetry.io/otel v1.33.0 // indirect 31 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 32 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 33 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect 34 | golang.org/x/net v0.39.0 // indirect 35 | golang.org/x/sync v0.13.0 // indirect 36 | golang.org/x/sys v0.32.0 // indirect 37 | golang.org/x/text v0.24.0 // indirect 38 | google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 39 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 40 | google.golang.org/grpc v1.68.1 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /plugins/service/grpc/plug.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/transport/grpc" 5 | "github.com/go-lynx/lynx/app" 6 | "github.com/go-lynx/lynx/app/factory" 7 | "github.com/go-lynx/lynx/plugins" 8 | ) 9 | 10 | // init 函数用于将 gRPC 服务器插件注册到全局插件工厂中。 11 | // 当该包被导入时,此函数会自动调用。 12 | // 它创建一个新的 ServiceGrpc 实例,并使用配置好的插件名称和配置前缀将其注册到插件工厂。 13 | func init() { 14 | // 调用全局插件工厂的 RegisterPlugin 方法进行插件注册 15 | // 传入插件名称、配置前缀和一个返回 plugins.Plugin 接口实例的函数 16 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 17 | // 创建并返回一个新的 ServiceGrpc 实例 18 | return NewServiceGrpc() 19 | }) 20 | } 21 | 22 | // GetGrpcServer 从插件管理器中获取 gRPC 服务器实例。 23 | // 该函数为应用程序的其他部分提供对底层 gRPC 服务器的访问, 24 | // 这些部分可能需要注册服务或使用服务器功能。 25 | // 26 | // 返回值: 27 | // - *grpc.Server: 配置好的 gRPC 服务器实例 28 | // 29 | // 注意: 如果插件未正确初始化,或者插件管理器找不到 gRPC 插件,此函数会触发 panic。 30 | func GetGrpcServer() *grpc.Server { 31 | // 从应用程序的插件管理器中获取指定名称的插件, 32 | // 并将其转换为 *ServiceGrpc 类型,然后返回其 server 字段 33 | return app.Lynx().GetPluginManager().GetPlugin(pluginName).(*ServiceGrpc).server 34 | } 35 | -------------------------------------------------------------------------------- /plugins/service/grpc/tls.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | 8 | "github.com/go-kratos/kratos/v2/transport/grpc" 9 | "github.com/go-lynx/lynx/app" 10 | ) 11 | 12 | // tlsLoad creates and configures TLS settings for the gRPC server. 13 | // tlsLoad 为 gRPC 服务器创建并配置 TLS 设置。 14 | // It performs the following operations: 15 | // 它执行以下操作: 16 | // - Loads the X.509 certificate and private key pair 17 | // - 加载 X.509 证书和私钥对 18 | // - Creates a certificate pool and adds the root CA certificate 19 | // - 创建证书池并添加根 CA 证书 20 | // - Configures TLS settings including client authentication type 21 | // - 配置 TLS 设置,包括客户端认证类型 22 | // 23 | // The method will panic if: 24 | // 该方法在以下情况下会发生 panic: 25 | // - The certificate and key pair cannot be loaded 26 | // - 无法加载证书和私钥对 27 | // - The root CA certificate cannot be added to the certificate pool 28 | // - 无法将根 CA 证书添加到证书池中 29 | // 30 | // Returns: 31 | // 返回: 32 | // - grpc.ServerOption: A configured TLS option for the gRPC server 33 | // - grpc.ServerOption: 为 gRPC 服务器配置好的 TLS 选项 34 | func (g *ServiceGrpc) tlsLoad() grpc.ServerOption { 35 | // Load the X.509 certificate and private key pair from the paths provided by the application. 36 | // 从应用程序提供的路径加载 X.509 证书和私钥对。 37 | // app.Lynx().Cert().GetCrt() returns the path to the certificate file. 38 | // app.Lynx().Cert().GetCrt() 返回证书文件的路径。 39 | // app.Lynx().Cert().GetKey() returns the path to the private key file. 40 | // app.Lynx().Cert().GetKey() 返回私钥文件的路径。 41 | // Get the certificate provider 42 | certProvider := app.Lynx().Certificate() 43 | if certProvider == nil { 44 | panic("certificate provider not configured") 45 | } 46 | 47 | // Load certificate and private key 48 | tlsCert, err := tls.X509KeyPair(certProvider.GetCertificate(), certProvider.GetPrivateKey()) 49 | if err != nil { 50 | // If there is an error loading the certificate and key pair, panic with the error 51 | panic(fmt.Errorf("failed to load X509 key pair: %v", err)) 52 | } 53 | 54 | // Create a new certificate pool to hold trusted root CA certificates 55 | certPool := x509.NewCertPool() 56 | 57 | // Attempt to add the root CA certificate (in PEM format) to the certificate pool 58 | if !certPool.AppendCertsFromPEM(certProvider.GetRootCACertificate()) { 59 | panic("failed to append root CA certificate to pool") 60 | } 61 | 62 | // Configure the TLS settings for the gRPC server. 63 | // 为 gRPC 服务器配置 TLS 设置。 64 | // Certificates: Set the server's certificate and private key pair. 65 | // Certificates: 设置服务器的证书和私钥对。 66 | // ClientCAs: Set the certificate pool containing trusted root CA certificates for client authentication. 67 | // ClientCAs: 设置包含受信任根 CA 证书的证书池,用于客户端认证。 68 | // ServerName: Set the server name, which is retrieved from the application configuration. 69 | // ServerName: 设置服务器名称,从应用程序配置中获取。 70 | // ClientAuth: Set the client authentication type based on the configuration. 71 | // ClientAuth: 根据配置设置客户端认证类型。 72 | return grpc.TLSConfig(&tls.Config{ 73 | Certificates: []tls.Certificate{tlsCert}, 74 | ClientCAs: certPool, 75 | ServerName: app.GetName(), 76 | ClientAuth: tls.ClientAuthType(g.conf.GetTlsAuthType()), 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /plugins/service/http/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Plugin for Lynx Framework 2 | 3 | This plugin provides HTTP server functionality for the Lynx framework. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/go-lynx/plugin-http 9 | ``` 10 | 11 | ## Features 12 | 13 | - HTTP/HTTPS server support 14 | - Middleware integration (tracing, logging, rate limiting, validation) 15 | - TLS support 16 | - Custom response encoding 17 | - Health checking 18 | - Event emission 19 | 20 | ## Configuration 21 | 22 | The plugin can be configured through the Lynx configuration system. Here's an example configuration: 23 | 24 | ```yaml 25 | lynx: 26 | http: 27 | network: tcp 28 | addr: :8080 29 | timeout: 1s 30 | tls: 31 | enabled: false 32 | cert: path/to/cert.pem 33 | key: path/to/key.pem 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```go 39 | import ( 40 | "github.com/go-lynx/plugin-http" 41 | "github.com/go-lynx/plugin-http/conf" 42 | ) 43 | 44 | func main() { 45 | // Create HTTP plugin with custom configuration 46 | httpPlugin := http.New( 47 | http.Weight(100), 48 | http.Config(&conf.Http{ 49 | Network: "tcp", 50 | Addr: ":8080", 51 | }), 52 | ) 53 | 54 | // Register the plugin with Lynx 55 | app.Lynx().RegisterPlugin(httpPlugin) 56 | } 57 | ``` 58 | 59 | ## Events 60 | 61 | The plugin emits the following events: 62 | 63 | - `EventPluginStarted`: When the HTTP server is successfully initialized 64 | - `EventPluginStopping`: When the server is about to stop 65 | - `EventPluginStopped`: When the server has been stopped 66 | 67 | ## Health Checks 68 | 69 | The plugin provides health check information including: 70 | - Server status 71 | - Address and network configuration 72 | - TLS status 73 | - Current connections (if available) 74 | 75 | ## Dependencies 76 | 77 | - github.com/go-kratos/kratos/v2 v2.8.3 78 | - github.com/go-lynx/lynx v1.0.0 79 | 80 | ## License 81 | 82 | Apache License 2.0 83 | -------------------------------------------------------------------------------- /plugins/service/http/conf/http.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package lynx.protobuf.plugin.http; 4 | 5 | option go_package = "github.com/go-lynx/lynx/plugins/service/http/conf;conf"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | // Http defines the configuration for the HTTP server plugin 10 | // Http 定义了 HTTP 服务器插件的配置信息。 11 | message http { 12 | // Network specifies the network type (e.g., "tcp", "unix") 13 | // Network 指定网络类型(例如 "tcp"、"unix")。 14 | string network = 1; 15 | 16 | // Addr specifies the address to listen on (e.g., ":8080", "localhost:8080") 17 | // Addr 指定 HTTP 服务器监听的地址(例如 ":8080"、"localhost:8080")。 18 | string addr = 2; 19 | 20 | // Tls indicates whether TLS/HTTPS is enabled 21 | // Tls 指示是否启用 TLS/HTTPS 加密。 22 | bool tls_enable = 3; 23 | 24 | // TlsAuthType specifies the TLS authentication type: 25 | // 0: No client auth 26 | // 1: Request client cert 27 | // 2: Require client cert 28 | // 3: Verify client cert 29 | // 4: Verify client cert if given 30 | // TlsAuthType 指定 TLS 客户端认证类型,具体含义如下: 31 | // 0: 不进行客户端认证 32 | // 1: 请求客户端证书,但不强制要求 33 | // 2: 强制要求客户端提供证书 34 | // 3: 验证客户端证书 35 | // 4: 若客户端提供证书,则进行验证 36 | int32 tls_auth_type = 4; 37 | 38 | // Timeout specifies the maximum duration for handling HTTP requests 39 | // Timeout 指定处理 HTTP 请求的最大时长。 40 | google.protobuf.Duration timeout = 5; 41 | } 42 | -------------------------------------------------------------------------------- /plugins/service/http/encoder.go: -------------------------------------------------------------------------------- 1 | // Package http 实现了 HTTP 相关的功能,包括响应编码和中间件。 2 | package http 3 | 4 | import ( 5 | "github.com/go-kratos/kratos/v2/errors" 6 | "github.com/go-kratos/kratos/v2/transport/http" 7 | "github.com/go-lynx/lynx/app/log" 8 | "google.golang.org/protobuf/runtime/protoimpl" 9 | nhttp "net/http" 10 | ) 11 | 12 | // Response 表示标准化的 HTTP 响应结构。 13 | // 它包含状态码、消息和可选的数据负载。 14 | type Response struct { 15 | // state 是 protobuf 消息的状态,用于内部处理。 16 | state protoimpl.MessageState 17 | // sizeCache 缓存消息的大小,用于优化序列化性能。 18 | sizeCache protoimpl.SizeCache 19 | // unknownFields 存储解析过程中遇到的未知字段。 20 | unknownFields protoimpl.UnknownFields 21 | 22 | // Code 是响应的状态码。 23 | Code int `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` 24 | // Message 是响应的描述消息。 25 | Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` 26 | // Data 是响应携带的具体数据。 27 | Data interface{} `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` 28 | } 29 | 30 | // ResponseEncoder 将响应数据编码为标准化的 JSON 格式。 31 | // 它将数据封装在一个状态码为 200、消息为 "success" 的 Response 结构体中。 32 | // w 是 HTTP 响应写入器,用于向客户端发送响应。 33 | // r 是 HTTP 请求对象,当前未使用。 34 | // data 是要编码的响应数据。 35 | // 返回编码过程中可能出现的错误。 36 | func ResponseEncoder(w http.ResponseWriter, r *http.Request, data interface{}) error { 37 | // 创建一个标准化的响应结构体 38 | res := &Response{ 39 | Code: 200, 40 | Message: "success", 41 | Data: data, 42 | } 43 | codec, _ := http.CodecForRequest(r, "Accept") 44 | body, err := codec.Marshal(res) 45 | if err != nil { 46 | w.WriteHeader(nhttp.StatusInternalServerError) 47 | return err 48 | } 49 | // 将 JSON 数据写入 HTTP 响应 50 | _, err = w.Write(body) 51 | if err != nil { 52 | // 写入失败,返回错误 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func EncodeErrorFunc(w http.ResponseWriter, r *http.Request, err error) { 59 | // 拿到error并转换成kratos Error实体 60 | se := errors.FromError(err) 61 | res := &Response{ 62 | Code: int(se.Code), 63 | Message: se.Message, 64 | } 65 | codec, _ := http.CodecForRequest(r, "Accept") 66 | body, err := codec.Marshal(res) 67 | if err != nil { 68 | w.WriteHeader(nhttp.StatusInternalServerError) 69 | return 70 | } 71 | w.Header().Set("Content-Type", "application/json") 72 | // 设置HTTP Status Code 73 | w.WriteHeader(nhttp.StatusInternalServerError) 74 | _, wErr := w.Write(body) 75 | if wErr != nil { 76 | log.Error("write error", wErr) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /plugins/service/http/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-lynx/lynx/plugins/service/http 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/go-kratos/kratos/contrib/middleware/validate/v2 v2.0.0-20250527152916-d6f5f00cf562 7 | github.com/go-kratos/kratos/v2 v2.8.4 8 | github.com/go-lynx/lynx v1.2.1 9 | go.opentelemetry.io/otel/trace v1.33.0 10 | google.golang.org/protobuf v1.36.5 11 | ) 12 | 13 | require ( 14 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 // indirect 15 | cel.dev/expr v0.22.0 // indirect 16 | dario.cat/mergo v1.0.0 // indirect 17 | github.com/antlr4-go/antlr/v4 v4.13.1 // indirect 18 | github.com/bufbuild/protovalidate-go v0.9.2 // indirect 19 | github.com/go-kratos/aegis v0.2.0 // indirect 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/go-playground/form/v4 v4.2.1 // indirect 23 | github.com/google/cel-go v0.24.1 // indirect 24 | github.com/google/uuid v1.6.0 // indirect 25 | github.com/gorilla/mux v1.8.1 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.19 // indirect 28 | github.com/rs/zerolog v1.34.0 // indirect 29 | github.com/stoewer/go-strcase v1.3.0 // indirect 30 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 31 | go.opentelemetry.io/otel v1.33.0 // indirect 32 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 33 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect 34 | golang.org/x/sync v0.13.0 // indirect 35 | golang.org/x/sys v0.32.0 // indirect 36 | golang.org/x/text v0.24.0 // indirect 37 | google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 38 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 39 | google.golang.org/grpc v1.68.1 // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /plugins/service/http/plug.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/go-kratos/kratos/v2/transport/http" 5 | "github.com/go-lynx/lynx/app" 6 | "github.com/go-lynx/lynx/app/factory" 7 | "github.com/go-lynx/lynx/plugins" 8 | ) 9 | 10 | func init() { 11 | factory.GlobalPluginFactory().RegisterPlugin(pluginName, confPrefix, func() plugins.Plugin { 12 | return NewServiceHttp() 13 | }) 14 | } 15 | 16 | // GetHttpServer retrieves the HTTP server instance from the plugin manager. 17 | // This function provides access to the underlying HTTP server for other 18 | // parts of the application that need to register handlers or access 19 | // server functionality. 20 | // 21 | // Returns: 22 | // - *http.Server: The configured HTTP server instance 23 | // 24 | // Note: This function will panic if the plugin is not properly initialized 25 | // or if the plugin manager cannot find the HTTP plugin. 26 | func GetHttpServer() *http.Server { 27 | return app.Lynx().GetPluginManager().GetPlugin(pluginName).(*ServiceHttp).server 28 | } 29 | -------------------------------------------------------------------------------- /plugins/service/http/tls.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | 8 | "github.com/go-kratos/kratos/v2/transport/http" 9 | "github.com/go-lynx/lynx/app" 10 | ) 11 | 12 | // tlsLoad creates and configures TLS settings for the gRPC server. 13 | // It performs the following operations: 14 | // - Loads the X.509 certificate and private key pair 15 | // - Creates a certificate pool and adds the root CA certificate 16 | // - Configures TLS settings including client authentication type 17 | // 18 | // The method will panic if: 19 | // - The certificate and key pair cannot be loaded 20 | // - The root CA certificate cannot be added to the certificate pool 21 | // 22 | // Returns: 23 | // - grpc.ServerOption: A configured TLS option for the Http server 24 | func (h *ServiceHttp) tlsLoad() http.ServerOption { 25 | // Get the certificate provider 26 | certProvider := app.Lynx().Certificate() 27 | if certProvider == nil { 28 | panic("certificate provider not configured") 29 | } 30 | 31 | // Load certificate and private key 32 | tlsCert, err := tls.X509KeyPair(certProvider.GetCertificate(), certProvider.GetPrivateKey()) 33 | if err != nil { 34 | panic(fmt.Errorf("failed to load X509 key pair: %v", err)) 35 | } 36 | 37 | // Create certificate pool and add root CA 38 | certPool := x509.NewCertPool() 39 | if !certPool.AppendCertsFromPEM(certProvider.GetRootCACertificate()) { 40 | panic("failed to append root CA certificate to pool") 41 | } 42 | 43 | return http.TLSConfig(&tls.Config{ 44 | Certificates: []tls.Certificate{tlsCert}, 45 | ClientCAs: certPool, 46 | ServerName: app.GetName(), 47 | ClientAuth: tls.ClientAuthType(h.conf.GetTlsAuthType()), 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /plugins/service/http/tracer.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/go-kratos/kratos/v2/middleware" 9 | "github.com/go-kratos/kratos/v2/transport" 10 | "github.com/go-lynx/lynx/app/log" 11 | "go.opentelemetry.io/otel/trace" 12 | "google.golang.org/protobuf/encoding/protojson" 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | const ( 17 | maxBodySize = 1024 * 1024 // 1MB 18 | contentTypeKey = "Content-Type" 19 | jsonContentType = "application/json" 20 | 21 | httpRequestLogFormat = "[HTTP Request] api=%s endpoint=%s client-ip=%s headers=%s body=%s" 22 | httpResponseLogFormat = "[HTTP Response] api=%s endpoint=%s duration=%v error=%v headers=%s body=%s" 23 | ) 24 | 25 | // getClientIP 获取客户端 IP 地址 26 | func getClientIP(header transport.Header) string { 27 | for _, key := range []string{"X-Forwarded-For", "X-Real-IP"} { 28 | if ip := header.Get(key); ip != "" { 29 | return ip 30 | } 31 | } 32 | return "unknown" 33 | } 34 | 35 | // safeProtoToJSON 安全地将 proto 消息转换为 JSON 36 | func safeProtoToJSON(msg proto.Message) (string, error) { 37 | body, err := protojson.Marshal(msg) 38 | if err != nil { 39 | return "", err 40 | } 41 | if len(body) > maxBodySize { 42 | return fmt.Sprintf("", len(body)), nil 43 | } 44 | return string(body), nil 45 | } 46 | 47 | // TracerLogPack 返回一个中间件,用于向响应添加跟踪 ID 和内容类型头。 48 | // 它从上下文提取跟踪 ID,并将其作为 "TraceID" 设置到响应头中。 49 | func TracerLogPack() middleware.Middleware { 50 | return func(handler middleware.Handler) middleware.Handler { 51 | return func(ctx context.Context, req interface{}) (reply interface{}, err error) { 52 | // 检查上下文是否已取消 53 | if ctx.Err() != nil { 54 | return nil, ctx.Err() 55 | } 56 | 57 | start := time.Now() 58 | span := trace.SpanContextFromContext(ctx) 59 | traceID := span.TraceID().String() 60 | spanID := span.SpanID().String() 61 | 62 | var tr transport.Transporter 63 | var ok bool 64 | 65 | // 提取请求信息 66 | if tr, ok = transport.FromServerContext(ctx); !ok { 67 | return handler(ctx, req) 68 | } 69 | 70 | endpoint := tr.Endpoint() 71 | clientIP := getClientIP(tr.RequestHeader()) 72 | 73 | // 设置响应头 74 | defer func() { 75 | header := tr.ReplyHeader() 76 | header.Set("Trace-Id", traceID) 77 | header.Set("Span-Id", spanID) 78 | // 只在确实是 JSON 响应时设置 Content-Type 79 | if _, ok := reply.(proto.Message); ok { 80 | header.Set(contentTypeKey, jsonContentType) 81 | } 82 | }() 83 | 84 | // 记录请求日志 85 | var reqBody string 86 | if msg, ok := req.(proto.Message); ok { 87 | if body, err := safeProtoToJSON(msg); err == nil { 88 | reqBody = body 89 | } else { 90 | reqBody = fmt.Sprintf("