├── .github ├── release.yml └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── check.sh ├── client ├── etcdv3 │ ├── client.go │ ├── client_test.go │ ├── config.go │ ├── config_test.go │ ├── intercept.go │ ├── intercept_test.go │ ├── lock.go │ ├── lock_test.go │ ├── watch.go │ └── watch_test.go ├── ftp │ ├── ftp.go │ └── ftp_test.go ├── grpc.go ├── grpc │ ├── balancer │ │ ├── base.go │ │ ├── base_test.go │ │ ├── p2c │ │ │ ├── basep2c │ │ │ │ └── base.go │ │ │ ├── leastloaded │ │ │ │ └── leastloaded.go │ │ │ ├── p2c.go │ │ │ └── p2c_test.go │ │ ├── swr.go │ │ └── swr_test.go │ ├── client.go │ ├── config.go │ ├── interceptor.go │ └── resolver │ │ ├── resolver.go │ │ └── resolver_test.go ├── grpc_test.go ├── proxy.go ├── proxy_test.go ├── rabbitmq │ ├── channel.go │ ├── connection.go │ ├── easy.go │ ├── queue.go │ ├── readme.md │ └── util.go └── sftp │ ├── sftp.go │ └── sftp_test.go ├── config └── config.go ├── core ├── app │ └── app.go ├── call │ └── call.go ├── component │ └── component.go ├── constant │ ├── config.go │ ├── env.go │ ├── key.go │ ├── module.go │ └── service.go ├── cycle │ ├── cycle.go │ └── cycle_test.go ├── ecode │ ├── code.go │ ├── status.go │ └── unified.go ├── env │ └── const.go ├── executor │ └── executor.go ├── goroutine │ ├── goroutine.go │ ├── init.go │ ├── parallel.go │ └── serial.go ├── hooks │ └── hooks.go ├── hostname │ └── hostname.go ├── logger │ ├── entry │ │ └── entry.go │ ├── es │ │ ├── doc.go │ │ ├── exec.go │ │ └── hook.go │ ├── logger.go │ ├── mongo │ │ ├── exec.go │ │ └── hook.go │ ├── queue │ │ ├── chan_queue.go │ │ ├── chan_queue_test.go │ │ ├── job.go │ │ ├── job_test.go │ │ ├── list_queue.go │ │ ├── list_queue_test.go │ │ ├── queue.go │ │ └── worker.go │ └── sql │ │ ├── exec.go │ │ └── hook.go ├── metric │ ├── counter.go │ ├── counter_test.go │ ├── gauge.go │ ├── gauge_test.go │ ├── histogram.go │ ├── histogram_test.go │ ├── metric.go │ ├── summary.go │ └── summary_test.go ├── ratelimit │ ├── ratelimit.go │ ├── ratelimit_test.go │ ├── reader.go │ ├── reader_test.go │ └── readme.md ├── resource │ ├── atomicduration.go │ ├── batcherror.go │ ├── breaker.go │ ├── breakers.go │ ├── nopbreaker.go │ ├── singleflight.go │ └── sourcemanager.go ├── singleton │ └── singleton.go ├── stack │ ├── defer.go │ ├── defer_test.go │ ├── stack.go │ └── stack_test.go ├── task │ ├── consistenthash │ │ └── consistenthash.go │ ├── cron │ │ ├── README.md │ │ ├── chain.go │ │ ├── constantdelay.go │ │ ├── cron.go │ │ ├── doc.go │ │ ├── option.go │ │ ├── parser.go │ │ └── spec.go │ ├── driver │ │ ├── driver.go │ │ ├── etcddriver.go │ │ ├── option.go │ │ ├── redisdriver.go │ │ ├── rediszsetdriver.go │ │ └── util.go │ ├── inodepool.go │ ├── irecentjobpacker.go │ ├── job_warpper.go │ ├── nodepool.go │ ├── option.go │ ├── recentjobpacker.go │ └── task.go └── trace │ ├── carrier.go │ ├── config.go │ ├── propagation.go │ ├── trace.go │ └── util.go ├── correct ├── format.go ├── format_test.go ├── fullwidth.go ├── fullwidth_test.go ├── halfwidth.go ├── halfwidth_test.go ├── html.go ├── html_test.go ├── strategery.go ├── strategery_test.go ├── unformat.go └── unformat_test.go ├── coverage.txt ├── crontab ├── task.go └── task_test.go ├── filter ├── filter.go ├── readme.md ├── result.go ├── search.go ├── skip.go └── trie.go ├── fmt.sh ├── go.mod ├── img ├── fontx │ └── font.go ├── img.go └── img_test.go ├── lang ├── i18n │ ├── i18n.go │ ├── i18n_test.go │ └── readme.md └── lang.go ├── mod.sh ├── nlpword ├── ac.go ├── ac_test.go ├── filter.go ├── filter_test.go ├── link.go ├── link_test.go ├── readme.md ├── trie.go └── trie_test.go ├── pagination ├── page.go └── page_test.go ├── registry ├── config.go ├── config_test.go ├── endpoint.go ├── endpoint_test.go ├── etcdv3 │ ├── config.go │ ├── config_test.go │ ├── registry.go │ └── registry_test.go ├── nop.go ├── nop_test.go ├── registry.go ├── registry_test.go ├── service.go ├── service_test.go └── update.go ├── server ├── server.go ├── server_test.go ├── xgin │ ├── config.go │ ├── const.go │ ├── const_test.go │ ├── error.go │ ├── error_test.go │ ├── middleware.go │ ├── middleware_test.go │ ├── server.go │ ├── server_test.go │ ├── status.go │ ├── status_test.go │ ├── websocket.go │ └── websocket_test.go ├── xgrpc │ ├── config.go │ ├── config_test.go │ ├── interceptor.go │ ├── recovery │ │ ├── interceptors.go │ │ └── options.go │ ├── server.go │ └── server_test.go ├── xhertz │ ├── config.go │ ├── middleware.go │ ├── middleware_test.go │ ├── server.go │ └── server_test.go └── xmonitor │ ├── config.go │ ├── config_test.go │ ├── init.go │ ├── init_test.go │ ├── server.go │ └── server_test.go ├── snowflake ├── readme.md ├── snowflake.go └── snowflake_test.go ├── stores ├── elasticsearch │ ├── elastic.go │ └── elastic_test.go ├── mongodb │ ├── clientmanager.go │ └── collection.go ├── null │ ├── README.md │ ├── bool.go │ ├── bool_test.go │ ├── byte.go │ ├── byte_test.go │ ├── bytes.go │ ├── bytes_test.go │ ├── convert │ │ ├── convert.go │ │ └── convert_test.go │ ├── ctime.go │ ├── ctime_test.go │ ├── date.go │ ├── date_test.go │ ├── datetime.go │ ├── datetime_test.go │ ├── float32.go │ ├── float32_test.go │ ├── float64.go │ ├── float64_test.go │ ├── format.go │ ├── int.go │ ├── int16.go │ ├── int16_test.go │ ├── int32.go │ ├── int32_test.go │ ├── int64.go │ ├── int64_test.go │ ├── int8.go │ ├── int8_test.go │ ├── int_test.go │ ├── json.go │ ├── json_test.go │ ├── string.go │ ├── string_test.go │ ├── time.go │ ├── time_test.go │ ├── timestamp.go │ ├── timestamp_test.go │ ├── uint.go │ ├── uint16.go │ ├── uint16_test.go │ ├── uint32.go │ ├── uint32_test.go │ ├── uint64.go │ ├── uint64_test.go │ ├── uint8.go │ ├── uint8_test.go │ ├── uint_test.go │ └── value.go ├── proxy │ ├── elasticsearch.go │ ├── mongodb.go │ ├── proxy.go │ ├── redis.go │ └── sql.go ├── redis │ ├── acl_commands.go │ ├── bitmap_commands.go │ ├── cluster_commands.go │ ├── command.go │ ├── generic_commands.go │ ├── geo_commands.go │ ├── hash_commands.go │ ├── hook.go │ ├── hyperloglog_commands.go │ ├── json_commands.go │ ├── list_commands.go │ ├── options.go │ ├── probabilistic_commands.go │ ├── pubsub_commands.go │ ├── redis.go │ ├── scripting_commands.go │ ├── search_commands.go │ ├── set_commands.go │ ├── sortedset_commands.go │ ├── stream_commands.go │ ├── string_commands.go │ ├── timeseries_commands.go │ └── utils.go ├── session │ ├── driver.go │ └── driver_test.go └── sql │ ├── client.go │ ├── interceptor.go │ ├── logger.go │ ├── pagination.go │ └── scope.go ├── toolkit ├── api │ ├── api.go │ ├── gin.go │ └── hertz.go ├── backend │ └── backend.go ├── base │ ├── init.go │ ├── init_test.go │ ├── install.go │ ├── install_compatible.go │ ├── install_compatible_test.go │ ├── install_test.go │ ├── mod.go │ ├── mod_test.go │ ├── path.go │ ├── path_test.go │ ├── query.go │ ├── query_test.go │ ├── repo.go │ ├── repo_test.go │ ├── util.go │ ├── vcs_url.go │ └── vcs_url_test.go ├── build │ └── build.go ├── change │ └── change.go ├── dao │ └── dao.go ├── frontend │ └── frontend.go ├── main.go ├── mod.sh ├── module │ ├── module.go │ ├── tpl_convert.go │ ├── tpl_module.go │ ├── tpl_proto.go │ └── tpl_service.go ├── project │ ├── add.go │ └── new.go ├── toolkit.toml ├── upgrade │ └── upgrade.go └── vue │ ├── interface.go │ ├── method.go │ ├── page.go │ └── vue.go ├── util ├── array.go ├── array_test.go ├── exec.go ├── exec_test.go ├── file.go ├── file_test.go ├── grpc.go ├── init.go ├── init_test.go ├── math.go ├── math_test.go ├── misc.go ├── misc_test.go ├── mongodb.go ├── mongodb_test.go ├── net.go ├── net_test.go ├── pb.go ├── pinyin.go ├── pinyin_test.go ├── regexp.go ├── regexp_test.go ├── scprng.go ├── scprng_test.go ├── string.go ├── string_test.go ├── time.go ├── time_test.go ├── url.go ├── url_test.go ├── util.go ├── util_test.go ├── var.go └── var_test.go ├── watch ├── builder.go ├── builder_test.go ├── logger.go ├── logger_test.go ├── options.go ├── options_test.go ├── watch.go └── watch_test.go └── worker ├── job └── job.go └── worker.go /.github/release.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '🚀 新功能' 3 | labels: ['feat', 'feature'] 4 | - title: '🛠️ 修复' 5 | labels: ['fix', 'bugfix'] 6 | - title: '💅 样式' 7 | labels: ['style'] 8 | - title: '📄 文档' 9 | labels: ['docs'] 10 | - title: '⚡️ 性能' 11 | labels: ['perf'] 12 | - title: '🧪 测试' 13 | labels: ['test'] 14 | - title: '♻️ 重构' 15 | labels: ['refactor'] 16 | - title: '📦 构建' 17 | labels: ['build'] 18 | - title: '🚨 补丁' 19 | labels: ['patch', 'hotfix'] 20 | - title: '🌐 发布' 21 | labels: ['release', 'publish'] 22 | - title: '🔧 流程' 23 | labels: ['ci', 'cd', 'workflow'] 24 | - title: '⚙️ 配置' 25 | labels: ['config', 'chore'] 26 | - title: '📁 文件' 27 | labels: ['file'] 28 | - title: '🎨 格式化' 29 | labels: ['format'] 30 | - title: '🔀 其他' 31 | labels: ['other', 'misc'] -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main,release-*,v2,v3 ] 6 | pull_request: 7 | branches: [ main,release-*,v2,v3 ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.24 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v3 23 | 24 | - name: Install linux deps 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get -y install --no-install-recommends libvips-dev libvips libvips-tools 28 | 29 | - name: Get dependencies 30 | run: | 31 | go mod tidy 32 | go mod download 33 | go mod graph 34 | go mod verify 35 | go mod why 36 | - name: Codecov 37 | uses: codecov/codecov-action@v1.0.6 38 | with: 39 | token: ${{secrets.CODECOV_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.idea 3 | /.vscode 4 | /.history 5 | /vendor 6 | /store 7 | /go.sum 8 | /test 9 | /coverage.txt 10 | /examples -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | skip-dirs: # 设置要忽略的目录 5 | - util 6 | - example 7 | - .*~ 8 | - api/swagger/docs 9 | - watch 10 | - goroutine 11 | skip-files: # 设置不需要检查的go源码文件,支持正则匹配,这里建议包括:_test.go 12 | - ".*\\.my\\.go$" 13 | - _test.go 14 | linters: 15 | enable: 16 | - errcheck 17 | - goimports 18 | - govet 19 | - staticcheck 20 | - revive 21 | issues: 22 | exclude-use-default: false 23 | max-issues-per-linter: 0 24 | max-same-issues: 0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ratel 2 | 3 | 4 | [![Go](https://github.com/abulo/ratel/v3/workflows/Go/badge.svg?branch=master)](https://github.com/abulo/ratel/v3/actions) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/abulo/ratel/v3)](https://goreportcard.com/report/github.com/abulo/ratel/v3) 6 | [![goproxy](https://goproxy.cn/stats/github.com/abulo/ratel/v3/badges/download-count.svg)](https://goproxy.cn/stats/github.com/abulo/ratel/v3/badges/download-count.svg) 7 | [![codecov](https://codecov.io/gh/abulo/ratel/branch/master/graph/badge.svg)](https://codecov.io/gh/abulo/ratel) 8 | [![Release](https://img.shields.io/github/v/release/abulo/ratel.svg?style=flat-square)](https://github.com/abulo/ratel/v3) 9 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 10 | 11 | 12 | ## Ratel 介绍 13 | 14 | Ratel 一套微服务治理框架,集成了各种工程实践的 web 和 rpc 框架。 15 | 16 | 优点: 17 | * 轻松获得支撑千万日活服务的稳定性 18 | * 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码 19 | * 微服务治理中间件可无缝集成到其它现有框架使用 20 | * 大量微服务治理和并发工具包 21 | 22 | ## Ratel 目的 23 | 24 | * 高效的性能 25 | * 简洁的语法 26 | * 广泛验证的工程效率 27 | * 极致的部署体验 28 | * 极低的服务端资源成本 29 | 30 | ## Ratel 思考 31 | 32 | * 保持简单,第一原则 33 | * 弹性设计,面向故障编程 34 | * 工具大于约定和文档 35 | * 高可用 36 | * 高并发 37 | * 易扩展 38 | * 对业务开发友好,封装复杂度 39 | * 约束做一件事只有一种方式 40 | 41 | 42 | ## Ratel 特性 43 | * 统一的指标采集 44 | * 链路追踪 45 | * 日志埋点 46 | * 统一错误处理 47 | * 动态配置 48 | * 安全策略 49 | * Debug 模式 等,可以极大的提高应用开发效率 50 | * 强大的工具支持,尽可能少的代码编写 51 | * 极简的接口 52 | * 完全兼容 net/http 53 | * 支持中间件,方便扩展 54 | * 高性能 55 | * 面向故障编程,弹性设计 56 | * 内建服务发现、负载均衡 57 | * 内建限流、熔断、降载,且自动触发,自动恢复 58 | * API 参数自动校验 59 | * 超时级联控制 60 | * 自动缓存控制 61 | * 链路跟踪、统计报警等 62 | * 高并发支撑,稳定保障了疫情期间每天的流量洪峰 -------------------------------------------------------------------------------- /client/etcdv3/intercept_test.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "google.golang.org/grpc" 8 | ) 9 | 10 | func Test_traceUnaryClientInterceptor(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | want grpc.UnaryClientInterceptor 14 | }{ 15 | // TODO: Add test cases. 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | if got := traceUnaryClientInterceptor(); !reflect.DeepEqual(got, tt.want) { 20 | t.Errorf("traceUnaryClientInterceptor() = %v, want %v", got, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | 26 | func Test_traceStreamClientInterceptor(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | want grpc.StreamClientInterceptor 30 | }{ 31 | // TODO: Add test cases. 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if got := traceStreamClientInterceptor(); !reflect.DeepEqual(got, tt.want) { 36 | t.Errorf("traceStreamClientInterceptor() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/etcdv3/lock.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.etcd.io/etcd/client/v3/concurrency" 8 | ) 9 | 10 | // Mutex ... 11 | type Mutex struct { 12 | s *concurrency.Session 13 | m *concurrency.Mutex 14 | } 15 | 16 | // NewMutex ... 17 | func (client *Client) NewMutex(key string, opts ...concurrency.SessionOption) (mutex *Mutex, err error) { 18 | mutex = &Mutex{} 19 | // 默认session ttl = 60s 20 | mutex.s, err = concurrency.NewSession(client.Client, opts...) 21 | if err != nil { 22 | return 23 | } 24 | mutex.m = concurrency.NewMutex(mutex.s, key) 25 | return 26 | } 27 | 28 | // Lock ... 29 | func (mutex *Mutex) Lock(timeout time.Duration) (err error) { 30 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 31 | defer cancel() 32 | return mutex.m.Lock(ctx) 33 | } 34 | 35 | // TryLock ... 36 | func (mutex *Mutex) TryLock(timeout time.Duration) (err error) { 37 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 38 | defer cancel() 39 | return mutex.m.Lock(ctx) 40 | } 41 | 42 | // Unlock ... 43 | func (mutex *Mutex) Unlock() (err error) { 44 | err = mutex.m.Unlock(context.TODO()) 45 | if err != nil { 46 | return 47 | } 48 | return mutex.s.Close() 49 | } 50 | -------------------------------------------------------------------------------- /client/etcdv3/lock_test.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "go.etcd.io/etcd/client/v3/concurrency" 9 | ) 10 | 11 | func TestClient_NewMutex(t *testing.T) { 12 | type args struct { 13 | key string 14 | opts []concurrency.SessionOption 15 | } 16 | tests := []struct { 17 | name string 18 | client *Client 19 | args args 20 | wantMutex *Mutex 21 | wantErr bool 22 | }{ 23 | // TODO: Add test cases. 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | gotMutex, err := tt.client.NewMutex(tt.args.key, tt.args.opts...) 28 | if (err != nil) != tt.wantErr { 29 | t.Errorf("Client.NewMutex() error = %v, wantErr %v", err, tt.wantErr) 30 | return 31 | } 32 | if !reflect.DeepEqual(gotMutex, tt.wantMutex) { 33 | t.Errorf("Client.NewMutex() = %v, want %v", gotMutex, tt.wantMutex) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestMutex_Lock(t *testing.T) { 40 | type args struct { 41 | timeout time.Duration 42 | } 43 | tests := []struct { 44 | name string 45 | mutex *Mutex 46 | args args 47 | wantErr bool 48 | }{ 49 | // TODO: Add test cases. 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | if err := tt.mutex.Lock(tt.args.timeout); (err != nil) != tt.wantErr { 54 | t.Errorf("Mutex.Lock() error = %v, wantErr %v", err, tt.wantErr) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestMutex_TryLock(t *testing.T) { 61 | type args struct { 62 | timeout time.Duration 63 | } 64 | tests := []struct { 65 | name string 66 | mutex *Mutex 67 | args args 68 | wantErr bool 69 | }{ 70 | // TODO: Add test cases. 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | if err := tt.mutex.TryLock(tt.args.timeout); (err != nil) != tt.wantErr { 75 | t.Errorf("Mutex.TryLock() error = %v, wantErr %v", err, tt.wantErr) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestMutex_Unlock(t *testing.T) { 82 | tests := []struct { 83 | name string 84 | mutex *Mutex 85 | wantErr bool 86 | }{ 87 | // TODO: Add test cases. 88 | } 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | if err := tt.mutex.Unlock(); (err != nil) != tt.wantErr { 92 | t.Errorf("Mutex.Unlock() error = %v, wantErr %v", err, tt.wantErr) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /client/etcdv3/watch_test.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "go.etcd.io/etcd/api/v3/mvccpb" 9 | clientv3 "go.etcd.io/etcd/client/v3" 10 | ) 11 | 12 | func TestWatch_C(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | w *Watch 16 | want chan *clientv3.Event 17 | }{ 18 | // TODO: Add test cases. 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if got := tt.w.C(); !reflect.DeepEqual(got, tt.want) { 23 | t.Errorf("Watch.C() = %v, want %v", got, tt.want) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestWatch_IncipientKeyValues(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | w *Watch 33 | want []*mvccpb.KeyValue 34 | }{ 35 | // TODO: Add test cases. 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := tt.w.IncipientKeyValues(); !reflect.DeepEqual(got, tt.want) { 40 | t.Errorf("Watch.IncipientKeyValues() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestClient_WatchPrefix(t *testing.T) { 47 | type args struct { 48 | ctx context.Context 49 | prefix string 50 | } 51 | tests := []struct { 52 | name string 53 | client *Client 54 | args args 55 | want *Watch 56 | wantErr bool 57 | }{ 58 | // TODO: Add test cases. 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | got, err := tt.client.WatchPrefix(tt.args.ctx, tt.args.prefix) 63 | if (err != nil) != tt.wantErr { 64 | t.Errorf("Client.WatchPrefix() error = %v, wantErr %v", err, tt.wantErr) 65 | return 66 | } 67 | if !reflect.DeepEqual(got, tt.want) { 68 | t.Errorf("Client.WatchPrefix() = %v, want %v", got, tt.want) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestWatch_Close(t *testing.T) { 75 | tests := []struct { 76 | name string 77 | w *Watch 78 | wantErr bool 79 | }{ 80 | // TODO: Add test cases. 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | if err := tt.w.Close(); (err != nil) != tt.wantErr { 85 | t.Errorf("Watch.Close() error = %v, wantErr %v", err, tt.wantErr) 86 | } 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /client/grpc.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/abulo/ratel/v3/client/grpc" 5 | "github.com/abulo/ratel/v3/registry/etcdv3" 6 | ) 7 | 8 | // Grpc ... 9 | type GrpcConfig struct { 10 | *grpc.Config 11 | } 12 | 13 | func NewGrpcConfig() *GrpcConfig { 14 | return &GrpcConfig{} 15 | } 16 | 17 | func (proxy *GrpcConfig) Store(client *grpc.Config) { 18 | proxy.Config = client 19 | } 20 | 21 | func (proxypool *Proxy) StoreGrpc(group string, proxy *GrpcConfig) { 22 | proxypool.m.Store(group, proxy) 23 | } 24 | 25 | func (proxypool *Proxy) LoadGrpc(group string) *grpc.Config { 26 | if f, ok := proxypool.m.Load(group); ok { 27 | return f.(*GrpcConfig).Config 28 | } 29 | return nil 30 | } 31 | 32 | type EtcdConfig struct { 33 | *etcdv3.Config 34 | } 35 | 36 | func NewEtcdConfig() *EtcdConfig { 37 | return &EtcdConfig{} 38 | } 39 | 40 | func (proxy *EtcdConfig) Store(client *etcdv3.Config) { 41 | proxy.Config = client 42 | } 43 | 44 | func (proxypool *Proxy) StoreEtcd(group string, proxy *EtcdConfig) { 45 | proxypool.m.Store(group, proxy) 46 | } 47 | 48 | func (proxypool *Proxy) LoadEtcd(group string) *etcdv3.Config { 49 | if f, ok := proxypool.m.Load(group); ok { 50 | return f.(*EtcdConfig).Config 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /client/grpc/balancer/p2c/basep2c/base.go: -------------------------------------------------------------------------------- 1 | package basep2c 2 | 3 | import "google.golang.org/grpc/balancer" 4 | 5 | // P2c ... 6 | type P2c interface { 7 | // Next returns next selected item. 8 | Next() (any, func(balancer.DoneInfo)) 9 | // Add a item. 10 | Add(any) 11 | } 12 | -------------------------------------------------------------------------------- /client/grpc/balancer/p2c/leastloaded/leastloaded.go: -------------------------------------------------------------------------------- 1 | package leastloaded 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/abulo/ratel/v3/client/grpc/balancer/p2c/basep2c" 10 | "google.golang.org/grpc/balancer" 11 | ) 12 | 13 | type leastLoadedNode struct { 14 | item any 15 | inflight int64 16 | } 17 | 18 | type leastLoaded struct { 19 | items []*leastLoadedNode 20 | mu sync.Mutex 21 | rand *rand.Rand 22 | } 23 | 24 | // New ... 25 | func New() basep2c.P2c { 26 | return &leastLoaded{ 27 | items: make([]*leastLoadedNode, 0), 28 | rand: rand.New(rand.NewSource(time.Now().Unix())), 29 | } 30 | } 31 | 32 | // Add ... 33 | func (p *leastLoaded) Add(item any) { 34 | p.items = append(p.items, &leastLoadedNode{item: item}) 35 | } 36 | 37 | // Next ... 38 | func (p *leastLoaded) Next() (any, func(balancer.DoneInfo)) { 39 | var sc, backsc *leastLoadedNode 40 | 41 | switch len(p.items) { 42 | case 0: 43 | return nil, func(balancer.DoneInfo) {} 44 | case 1: 45 | sc = p.items[0] 46 | default: 47 | // rand needs lock 48 | p.mu.Lock() 49 | a := p.rand.Intn(len(p.items)) 50 | b := p.rand.Intn(len(p.items) - 1) 51 | p.mu.Unlock() 52 | 53 | if b >= a { 54 | b = b + 1 55 | } 56 | sc, backsc = p.items[a], p.items[b] 57 | 58 | // choose the least loaded item based on inflight 59 | if sc.inflight > backsc.inflight { 60 | sc, _ = backsc, sc 61 | } 62 | } 63 | 64 | atomic.AddInt64(&sc.inflight, 1) 65 | 66 | return sc.item, func(balancer.DoneInfo) { 67 | atomic.AddInt64(&sc.inflight, -1) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/grpc/balancer/p2c/p2c.go: -------------------------------------------------------------------------------- 1 | package p2c 2 | 3 | import ( 4 | "github.com/abulo/ratel/v3/client/grpc/balancer/p2c/basep2c" 5 | "github.com/abulo/ratel/v3/client/grpc/balancer/p2c/leastloaded" 6 | "google.golang.org/grpc/balancer" 7 | "google.golang.org/grpc/balancer/base" 8 | "google.golang.org/grpc/grpclog" 9 | ) 10 | 11 | // Name is the name of p2c with least loaded balancer. 12 | const ( 13 | Name = "p2c_least_loaded" 14 | ) 15 | 16 | // newBuilder creates a new balance builder. 17 | func newBuilder() balancer.Builder { 18 | return base.NewBalancerBuilder(Name, &p2cPickerBuilder{}, base.Config{HealthCheck: true}) 19 | } 20 | 21 | func init() { 22 | balancer.Register(newBuilder()) 23 | } 24 | 25 | type p2cPickerBuilder struct{} 26 | 27 | // Build ... 28 | func (*p2cPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker { 29 | grpclog.Infof("p2cPickerBuilder: newPicker called with readySCs: %v", info.ReadySCs) 30 | if len(info.ReadySCs) == 0 { 31 | return base.NewErrPicker(balancer.ErrNoSubConnAvailable) 32 | } 33 | 34 | var p2c = leastloaded.New() 35 | 36 | for sc := range info.ReadySCs { 37 | p2c.Add(sc) 38 | } 39 | 40 | rp := &p2cPicker{ 41 | p2c: p2c, 42 | } 43 | return rp 44 | } 45 | 46 | type p2cPicker struct { 47 | p2c basep2c.P2c 48 | } 49 | 50 | // Pick ... 51 | func (p *p2cPicker) Pick(opts balancer.PickInfo) (balancer.PickResult, error) { 52 | item, done := p.p2c.Next() 53 | if item == nil { 54 | return balancer.PickResult{}, balancer.ErrNoSubConnAvailable 55 | } 56 | 57 | return balancer.PickResult{SubConn: item.(balancer.SubConn), Done: done}, nil 58 | } 59 | -------------------------------------------------------------------------------- /client/grpc/balancer/p2c/p2c_test.go: -------------------------------------------------------------------------------- 1 | package p2c 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "google.golang.org/grpc/balancer" 8 | "google.golang.org/grpc/balancer/base" 9 | ) 10 | 11 | func Test_newBuilder(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | want balancer.Builder 15 | }{ 16 | // TODO: Add test cases. 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | if got := newBuilder(); !reflect.DeepEqual(got, tt.want) { 21 | t.Errorf("newBuilder() = %v, want %v", got, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func Test_p2cPickerBuilder_Build(t *testing.T) { 28 | type args struct { 29 | info base.PickerBuildInfo 30 | } 31 | tests := []struct { 32 | name string 33 | p *p2cPickerBuilder 34 | args args 35 | want balancer.Picker 36 | }{ 37 | // TODO: Add test cases. 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := tt.p.Build(tt.args.info); !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("p2cPickerBuilder.Build() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func Test_p2cPicker_Pick(t *testing.T) { 49 | type args struct { 50 | opts balancer.PickInfo 51 | } 52 | tests := []struct { 53 | name string 54 | p *p2cPicker 55 | args args 56 | want balancer.PickResult 57 | wantErr bool 58 | }{ 59 | // TODO: Add test cases. 60 | } 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | got, err := tt.p.Pick(tt.args.opts) 64 | if (err != nil) != tt.wantErr { 65 | t.Errorf("p2cPicker.Pick() error = %v, wantErr %v", err, tt.wantErr) 66 | return 67 | } 68 | if !reflect.DeepEqual(got, tt.want) { 69 | t.Errorf("p2cPicker.Pick() = %v, want %v", got, tt.want) 70 | } 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /client/grpc/balancer/swr_test.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "google.golang.org/grpc/balancer" 8 | ) 9 | 10 | func Test_swrPickerBuilder_Build(t *testing.T) { 11 | type args struct { 12 | info PickerBuildInfo 13 | } 14 | tests := []struct { 15 | name string 16 | s swrPickerBuilder 17 | args args 18 | want balancer.Picker 19 | }{ 20 | // TODO: Add test cases. 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | if got := tt.s.Build(tt.args.info); !reflect.DeepEqual(got, tt.want) { 25 | t.Errorf("swrPickerBuilder.Build() = %v, want %v", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func Test_newSWRPicker(t *testing.T) { 32 | type args struct { 33 | info PickerBuildInfo 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want *swrPicker 39 | }{ 40 | // TODO: Add test cases. 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if got := newSWRPicker(tt.args.info); !reflect.DeepEqual(got, tt.want) { 45 | t.Errorf("newSWRPicker() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func Test_swrPicker_Pick(t *testing.T) { 52 | type args struct { 53 | info balancer.PickInfo 54 | } 55 | tests := []struct { 56 | name string 57 | p *swrPicker 58 | args args 59 | want balancer.PickResult 60 | wantErr bool 61 | }{ 62 | // TODO: Add test cases. 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | got, err := tt.p.Pick(tt.args.info) 67 | if (err != nil) != tt.wantErr { 68 | t.Errorf("swrPicker.Pick() error = %v, wantErr %v", err, tt.wantErr) 69 | return 70 | } 71 | if !reflect.DeepEqual(got, tt.want) { 72 | t.Errorf("swrPicker.Pick() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func Test_swrPicker_parseBuildInfo(t *testing.T) { 79 | type args struct { 80 | info PickerBuildInfo 81 | } 82 | tests := []struct { 83 | name string 84 | p *swrPicker 85 | args args 86 | }{ 87 | // TODO: Add test cases. 88 | } 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | tt.p.parseBuildInfo(tt.args.info) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /client/grpc/client.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/abulo/ratel/v3/client/grpc/resolver" 9 | "github.com/abulo/ratel/v3/core/ecode" 10 | "github.com/abulo/ratel/v3/core/logger" 11 | "github.com/sirupsen/logrus" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/insecure" 14 | ) 15 | 16 | func newGRPCClient(config *Config) (*grpc.ClientConn, error) { 17 | var ctx = context.Background() 18 | dialOptions := getDialOptions(config) 19 | // 默认配置使用block 20 | if config.Block { 21 | if config.DialTimeout > time.Duration(0) { 22 | var cancel context.CancelFunc 23 | ctx, cancel = context.WithTimeout(ctx, config.DialTimeout) 24 | defer cancel() 25 | } 26 | dialOptions = append(dialOptions, grpc.WithBlock()) 27 | } 28 | 29 | conn, err := grpc.DialContext(ctx, config.Address, dialOptions...) 30 | // conn, err := grpc.DialContext(ctx, config.Address, append(dialOptions, grpc.WithBlock())...) 31 | if err != nil { 32 | logger.Logger.WithFields(logrus.Fields{ 33 | "err": err, 34 | }).Panic("dial grpc server failed, connect without block,", ecode.ErrKindRequestErr) 35 | conn, err = grpc.DialContext(context.Background(), config.Address, dialOptions...) 36 | 37 | if err != nil { 38 | logger.Logger.WithFields(logrus.Fields{ 39 | "err": err, 40 | }).Panic("connect without block failed", ecode.ErrKindRequestErr) 41 | return nil, err 42 | } 43 | } 44 | logger.Logger.Info("start grpc client") 45 | return conn, nil 46 | } 47 | 48 | func getDialOptions(config *Config) []grpc.DialOption { 49 | dialOptions := config.dialOptions 50 | 51 | if config.KeepAlive != nil { 52 | dialOptions = append(dialOptions, grpc.WithKeepaliveParams(*config.KeepAlive)) 53 | } 54 | 55 | dialOptions = append(dialOptions, 56 | grpc.WithTransportCredentials(insecure.NewCredentials()), 57 | grpc.WithResolvers(resolver.NewEtcdBuilder(config.Etcd.GetNode(), config.Etcd.MustSingleton())), 58 | grpc.WithDisableServiceConfig(), 59 | ) 60 | 61 | svcCfg := fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, config.BalancerName) 62 | dialOptions = append(dialOptions, grpc.WithDefaultServiceConfig(svcCfg)) 63 | 64 | return dialOptions 65 | } 66 | -------------------------------------------------------------------------------- /client/proxy.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "sync" 4 | 5 | // Proxy 代理池 6 | type Proxy struct { 7 | m sync.Map 8 | } 9 | 10 | // NewProxy 代理池 11 | func NewClientProxy() *Proxy { 12 | return &Proxy{ 13 | m: sync.Map{}, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/proxy_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewClientProxy(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | want *Proxy 12 | }{ 13 | // TODO: Add test cases. 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | if got := NewClientProxy(); !reflect.DeepEqual(got, tt.want) { 18 | t.Errorf("NewClientProxy() = %v, want %v", got, tt.want) 19 | } 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/rabbitmq/easy.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "github.com/streadway/amqp" 5 | ) 6 | 7 | type Consumer struct { 8 | c *Connection 9 | } 10 | 11 | // Receive 持续接收消息并消费。如果期望只接收一次消息,可以使用 Get 方法。 12 | // 此方法是异步方法,内部使用了 go routine 执行接收操作,因此即便没有消息 13 | // 可以接收时,该方法也不会阻塞。 14 | // 详见 Channel.ReceiveOpts 15 | func (c *Consumer) Receive(queue string, opts *ReceiveOpts, lis ReceiveListener) { 16 | c.c.RegisterAndExec(func(key string, ch *Channel) { 17 | err := ch.ReceiveOpts(queue, lis.Consumer, opts) 18 | if err == nil { 19 | lis.Remove(key, ch) 20 | } 21 | lis.Finish(err) 22 | }) 23 | } 24 | 25 | // Get When autoAck is true, the server will automatically acknowledge this message so you don't have to. 26 | // But if you are unable to fully process this message before the channel or connection is closed, 27 | // the message will not get requeued 28 | func (c *Consumer) Get(queue string, autoAck bool) (*amqp.Delivery, bool, error) { 29 | ch, err := c.c.Channel() 30 | if err != nil { 31 | return nil, false, err 32 | } 33 | defer ch.Close() 34 | msg, ok, err := ch.Get(queue, autoAck) 35 | if err != nil { 36 | return nil, false, err 37 | } 38 | return &msg, ok, err 39 | } 40 | 41 | type Producer struct { 42 | c *Connection 43 | } 44 | 45 | // Send 发送消息。 46 | // 参数 body 即需要发送的消息。 47 | // 参数 opts 即发送消息需要配置的选项。如果 opts 为 nil,则表示使用默认配置。可以通过配置 SendOpts.retryable 48 | // 启用消息重发的能力。请注意,由于消息重发使用的是同步的方式处理 ack,因此启用消息重发会极大降低 QPS。 49 | func (p *Producer) Send(exchange string, routingKey string, body []byte, opts *SendOpts) error { 50 | ch, err := p.c.Channel() 51 | if err != nil { 52 | return err 53 | } 54 | defer ch.Close() 55 | return ch.SendOpts(exchange, routingKey, body, opts) 56 | } 57 | -------------------------------------------------------------------------------- /core/call/call.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import "runtime" 4 | 5 | func Caller(skip int) (fn string, file string, lineNo int) { 6 | pc, file, lineNo, _ := runtime.Caller(skip) 7 | fn = runtime.FuncForPC(pc).Name() 8 | return fn, file, lineNo 9 | } 10 | -------------------------------------------------------------------------------- /core/component/component.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import "github.com/abulo/ratel/v3/core/metric" 4 | 5 | type Component interface { 6 | // Start blocks until the channel is closed or an error occurs. 7 | // The component will stop running when the channel is closed. 8 | Start(<-chan struct{}) error 9 | 10 | ShouldBeLeader() bool 11 | } 12 | 13 | var _ Component = ComponentFunc(nil) 14 | 15 | type ComponentFunc func(<-chan struct{}) error 16 | 17 | func (f ComponentFunc) Start(stop <-chan struct{}) error { 18 | return f(stop) 19 | } 20 | 21 | func (f ComponentFunc) ShouldBeLeader() bool { 22 | return false 23 | } 24 | 25 | // Component manager, aggregate multiple components to one 26 | type Manager interface { 27 | Component 28 | AddComponent(...Component) error 29 | } 30 | 31 | // Component builder, build component with injecting govern plugin 32 | type Builder interface { 33 | WithComponentManager(Manager) Builder 34 | WithMetrics(metric.Metrics) Builder 35 | Build() Component 36 | } 37 | -------------------------------------------------------------------------------- /core/constant/config.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // DefaultNamespace ... 4 | var ( 5 | // ConfigPrefix 配置前缀 6 | // ConfigPrefix = "ratel" 7 | // DefaultNamespace 默认命名空间 8 | DefaultNamespace = "ratel" 9 | ) 10 | -------------------------------------------------------------------------------- /core/constant/env.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // EnvKeySentinelLogDir ... 4 | const ( 5 | // EnvKeySentinelLogDir ... 6 | EnvKeySentinelLogDir = "SENTINEL_LOG_DIR" 7 | // EnvKeySentinelAppName ... 8 | EnvKeySentinelAppName = "SENTINEL_APP_NAME" 9 | ) 10 | 11 | // EnvAppName ... 12 | const ( 13 | // EnvAppName ... 14 | EnvAppName = "APP_NAME" 15 | EnvAppID = "APP_ID" 16 | EnvDeployment = "APP_DEPLOYMENT" 17 | EnvAppMode = "APP_MODE" 18 | EnvAppRegion = "APP_REGION" 19 | EnvAppZone = "APP_ZONE" 20 | EnvAppHost = "APP_HOST" 21 | EnvAppInstance = "APP_INSTANCE" // application unique instance id. 22 | EnvPodIP = "POD_IP" //k8s环境 23 | EnvPodNAME = "POD_NAME" //k8s环境 24 | ) 25 | 26 | // DefaultDeployment ... 27 | const ( 28 | // DefaultDeployment ... 29 | DefaultDeployment = "" 30 | // DefaultRegion ... 31 | DefaultRegion = "" 32 | // DefaultZone ... 33 | DefaultZone = "" 34 | ) 35 | 36 | // KeyBalanceGroup ... 37 | const ( 38 | // KeyBalanceGroup ... 39 | KeyBalanceGroup = "__group" 40 | 41 | // DefaultBalanceGroup ... 42 | DefaultBalanceGroup = "default" 43 | ) 44 | -------------------------------------------------------------------------------- /core/constant/key.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // KeyRouteConfig ... 4 | const ( 5 | // KeyRouteConfig ... 6 | KeyRouteConfig = "__route_config_" 7 | 8 | // KeyRouteGroup ... 9 | KeyRouteGroup = "__route_group_" 10 | 11 | // KeyProviderConfig ... 12 | KeyProviderConfig = "__provider_config_" 13 | 14 | // KeyConsumerConfig ... 15 | KeyConsumerConfig = "__consumer_config_" 16 | 17 | // KeyServiceInfo 18 | KeyServiceInfo = "__service_info_" 19 | ) 20 | -------------------------------------------------------------------------------- /core/constant/module.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | type Module int 4 | 5 | const ( 6 | ModuleInvalid = Module(iota) 7 | 8 | ModuleClientGrpc 9 | ModuleClientResty 10 | ModuleClientRedis 11 | ModuleClientRocketMQ 12 | ModuleClientEtcd 13 | 14 | ModuleRegistryEtcd 15 | 16 | ModuleStoreMongoDB 17 | ModuleStoreGorm 18 | ModuleStoreTableStore 19 | ) 20 | -------------------------------------------------------------------------------- /core/constant/service.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // ServiceKind service kind 4 | type ServiceKind uint8 5 | 6 | // ServiceUnknown ... 7 | const ( 8 | //ServiceUnknown service non-name 9 | ServiceUnknown ServiceKind = iota 10 | //ServiceProvider service provider 11 | ServiceProvider 12 | //ServiceMonitor service monitor 13 | ServiceMonitor 14 | //ServiceConsumer service consumer 15 | ServiceConsumer 16 | ) 17 | 18 | var serviceKinds = make(map[ServiceKind]string) 19 | 20 | func init() { 21 | serviceKinds[ServiceUnknown] = "unknown" 22 | serviceKinds[ServiceProvider] = "providers" 23 | serviceKinds[ServiceMonitor] = "monitor" 24 | serviceKinds[ServiceConsumer] = "consumers" 25 | } 26 | 27 | // String ... 28 | func (sk ServiceKind) String() string { 29 | if s, ok := serviceKinds[sk]; ok { 30 | return s 31 | } 32 | return "unknown" 33 | } 34 | -------------------------------------------------------------------------------- /core/cycle/cycle.go: -------------------------------------------------------------------------------- 1 | package cycle 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // Cycle .. 9 | type Cycle struct { 10 | mu *sync.Mutex 11 | wg *sync.WaitGroup 12 | done chan struct{} 13 | quit chan error 14 | closing uint32 15 | waiting uint32 16 | // works []func() error 17 | } 18 | 19 | // NewCycle new a cycle life 20 | func NewCycle() *Cycle { 21 | return &Cycle{ 22 | mu: &sync.Mutex{}, 23 | wg: &sync.WaitGroup{}, 24 | done: make(chan struct{}), 25 | quit: make(chan error), 26 | closing: 0, 27 | waiting: 0, 28 | } 29 | } 30 | 31 | // Run a new goroutine 32 | func (c *Cycle) Run(fn func() error) { 33 | c.mu.Lock() 34 | //todo add check options panic before waiting 35 | defer c.mu.Unlock() 36 | c.wg.Add(1) 37 | go func(c *Cycle) { 38 | defer c.wg.Done() 39 | if err := fn(); err != nil { 40 | c.quit <- err 41 | } 42 | }(c) 43 | } 44 | 45 | // Done block and return a chan error 46 | func (c *Cycle) Done() <-chan struct{} { 47 | if atomic.CompareAndSwapUint32(&c.waiting, 0, 1) { 48 | go func(c *Cycle) { 49 | c.mu.Lock() 50 | defer c.mu.Unlock() 51 | c.wg.Wait() 52 | close(c.done) 53 | }(c) 54 | } 55 | return c.done 56 | } 57 | 58 | // DoneAndClose .. 59 | func (c *Cycle) DoneAndClose() { 60 | <-c.Done() 61 | c.Close() 62 | } 63 | 64 | // Close .. 65 | func (c *Cycle) Close() { 66 | c.mu.Lock() 67 | defer c.mu.Unlock() 68 | if atomic.CompareAndSwapUint32(&c.closing, 0, 1) { 69 | close(c.quit) 70 | } 71 | } 72 | 73 | // Wait blocked for a life cycle 74 | func (c *Cycle) Wait() <-chan error { 75 | return c.quit 76 | } 77 | -------------------------------------------------------------------------------- /core/cycle/cycle_test.go: -------------------------------------------------------------------------------- 1 | package cycle 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewCycle(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | want *Cycle 12 | }{ 13 | // TODO: Add test cases. 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | if got := NewCycle(); !reflect.DeepEqual(got, tt.want) { 18 | t.Errorf("NewCycle() = %v, want %v", got, tt.want) 19 | } 20 | }) 21 | } 22 | } 23 | 24 | func TestCycle_Run(t *testing.T) { 25 | type args struct { 26 | fn func() error 27 | } 28 | tests := []struct { 29 | name string 30 | c *Cycle 31 | args args 32 | }{ 33 | // TODO: Add test cases. 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | tt.c.Run(tt.args.fn) 38 | }) 39 | } 40 | } 41 | 42 | func TestCycle_Done(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | c *Cycle 46 | want <-chan struct{} 47 | }{ 48 | // TODO: Add test cases. 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | if got := tt.c.Done(); !reflect.DeepEqual(got, tt.want) { 53 | t.Errorf("Cycle.Done() = %v, want %v", got, tt.want) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func TestCycle_DoneAndClose(t *testing.T) { 60 | tests := []struct { 61 | name string 62 | c *Cycle 63 | }{ 64 | // TODO: Add test cases. 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | tt.c.DoneAndClose() 69 | }) 70 | } 71 | } 72 | 73 | func TestCycle_Close(t *testing.T) { 74 | tests := []struct { 75 | name string 76 | c *Cycle 77 | }{ 78 | // TODO: Add test cases. 79 | } 80 | for _, tt := range tests { 81 | t.Run(tt.name, func(t *testing.T) { 82 | tt.c.Close() 83 | }) 84 | } 85 | } 86 | 87 | func TestCycle_Wait(t *testing.T) { 88 | tests := []struct { 89 | name string 90 | c *Cycle 91 | want <-chan error 92 | }{ 93 | // TODO: Add test cases. 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | if got := tt.c.Wait(); !reflect.DeepEqual(got, tt.want) { 98 | t.Errorf("Cycle.Wait() = %v, want %v", got, tt.want) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/ecode/code.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Douyu 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ecode 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/abulo/ratel/v3/core/logger" 21 | spb "google.golang.org/genproto/googleapis/rpc/status" 22 | "google.golang.org/grpc/codes" 23 | "google.golang.org/grpc/status" 24 | any "google.golang.org/protobuf/types/known/anypb" 25 | ) 26 | 27 | // EcodeNum 低于10000均为系统错误码,业务错误码请使用10000以上 28 | const EcodeNum int32 = 9999 29 | 30 | var ( 31 | aid int 32 | maxCustomizeCode = 9999 33 | _codes sync.Map 34 | // OK ... 35 | OK = add(int(codes.OK), "OK") 36 | ) 37 | 38 | // Add ... 39 | func Add(code int, message string) *spbStatus { 40 | if code > maxCustomizeCode { 41 | logger.Logger.Panic("customize code must less than 9999,", code) 42 | } 43 | 44 | return add(aid*10000+code, message) 45 | } 46 | 47 | func add(code int, message string) *spbStatus { 48 | status := &spbStatus{ 49 | &spb.Status{ 50 | Code: int32(code), 51 | Message: message, 52 | Details: make([]*any.Any, 0), 53 | }, 54 | } 55 | _codes.Store(code, status) 56 | return status 57 | } 58 | 59 | // ExtractCodes cause from error to ecode. 60 | func ExtractCodes(e error) *spbStatus { 61 | if e == nil { 62 | return OK 63 | } 64 | gst, _ := status.FromError(e) 65 | return &spbStatus{ 66 | &spb.Status{ 67 | Code: int32(gst.Code()), 68 | Message: gst.Message(), 69 | Details: make([]*any.Any, 0), 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/goroutine/goroutine.go: -------------------------------------------------------------------------------- 1 | package goroutine 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/abulo/ratel/v3/core/logger" 8 | "github.com/codegangsta/inject" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Serial 串行 13 | func Serial(fns ...func()) func() { 14 | return func() { 15 | for _, fn := range fns { 16 | fn() 17 | } 18 | } 19 | } 20 | 21 | // Parallel 并发执行 22 | func Parallel(fns ...func()) func() { 23 | var wg sync.WaitGroup 24 | return func() { 25 | wg.Add(len(fns)) 26 | for _, fn := range fns { 27 | go try2(fn, wg.Done) 28 | } 29 | wg.Wait() 30 | } 31 | } 32 | 33 | // RestrictParallel 并发,最大并发量restrict 34 | func RestrictParallel(restrict int, fns ...func()) func() { 35 | var channel = make(chan struct{}, restrict) 36 | return func() { 37 | var wg sync.WaitGroup 38 | for _, fn := range fns { 39 | wg.Add(1) 40 | go func(fn func()) { 41 | defer wg.Done() 42 | channel <- struct{}{} 43 | try2(fn, nil) 44 | <-channel 45 | }(fn) 46 | } 47 | wg.Wait() 48 | close(channel) 49 | } 50 | } 51 | 52 | // GoDirect ... 53 | func GoDirect(fn any, args ...any) { 54 | var inj = inject.New() 55 | for _, arg := range args { 56 | inj.Map(arg) 57 | } 58 | 59 | go func() { 60 | defer func() { 61 | if err := recover(); err != nil { 62 | logger.Logger.WithFields(logrus.Fields{ 63 | "err": err, 64 | }).Error("recover") 65 | } 66 | }() 67 | // 忽略返回值, goroutine执行的返回值通常都会忽略掉 68 | _, err := inj.Invoke(fn) 69 | if err != nil { 70 | logger.Logger.WithFields(logrus.Fields{ 71 | "err": err, 72 | }).Error("inject") 73 | return 74 | } 75 | }() 76 | } 77 | 78 | // Go goroutine 79 | func Go(fn func()) { 80 | go try2(fn, nil) 81 | } 82 | 83 | // DelayGo goroutine 84 | func DelayGo(delay time.Duration, fn func()) { 85 | go func() { 86 | defer func() { 87 | if err := recover(); err != nil { 88 | logger.Logger.WithFields(logrus.Fields{ 89 | "err": err, 90 | }).Error("inject") 91 | } 92 | }() 93 | time.Sleep(delay) 94 | fn() 95 | }() 96 | } 97 | 98 | // SafeGo safe go 99 | func SafeGo(fn func(), rec func(error)) { 100 | go func() { 101 | err := try2(fn, nil) 102 | if err != nil { 103 | rec(err) 104 | } 105 | }() 106 | } 107 | -------------------------------------------------------------------------------- /core/goroutine/init.go: -------------------------------------------------------------------------------- 1 | package goroutine 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/abulo/ratel/v3/core/logger" 7 | "github.com/abulo/ratel/v3/util" 8 | "github.com/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func try(fn func() error, cleaner func()) (ret error) { 13 | if cleaner != nil { 14 | defer cleaner() 15 | } 16 | defer func() { 17 | if err := recover(); err != nil { 18 | logger.Logger.WithFields(logrus.Fields{ 19 | "err": err, 20 | }).Error("recover") 21 | if _, ok := err.(error); ok { 22 | ret = err.(error) 23 | } else { 24 | ret = fmt.Errorf("%+v", err) 25 | } 26 | ret = errors.Wrap(ret, fmt.Sprintf("%s", util.FunctionName(fn))) 27 | } 28 | }() 29 | return fn() 30 | } 31 | 32 | func try2(fn func(), cleaner func()) (ret error) { 33 | if cleaner != nil { 34 | defer cleaner() 35 | } 36 | defer func() { 37 | if err := recover(); err != nil { 38 | logger.Logger.WithFields(logrus.Fields{ 39 | "err": err, 40 | }).Error("recover") 41 | if _, ok := err.(error); ok { 42 | ret = err.(error) 43 | } else { 44 | ret = fmt.Errorf("%+v", err) 45 | } 46 | } 47 | }() 48 | fn() 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /core/goroutine/parallel.go: -------------------------------------------------------------------------------- 1 | package goroutine 2 | 3 | import ( 4 | "sync" 5 | 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | // ParallelWithError ... 10 | func ParallelWithError(fns ...func() error) func() error { 11 | return func() error { 12 | eg := errgroup.Group{} 13 | for _, fn := range fns { 14 | eg.Go(fn) 15 | } 16 | 17 | return eg.Wait() 18 | } 19 | } 20 | 21 | // ParallelWithErrorChan calls the passed functions in a goroutine, returns a chan of errors. 22 | // fns会并发执行,chan error 23 | func ParallelWithErrorChan(fns ...func() error) chan error { 24 | total := len(fns) 25 | errs := make(chan error, total) 26 | 27 | var wg sync.WaitGroup 28 | wg.Add(total) 29 | 30 | go func(errs chan error) { 31 | wg.Wait() 32 | close(errs) 33 | }(errs) 34 | 35 | for _, fn := range fns { 36 | go func(fn func() error, errs chan error) { 37 | defer wg.Done() 38 | errs <- try(fn, nil) 39 | }(fn, errs) 40 | } 41 | 42 | return errs 43 | } 44 | 45 | // RestrictParallelWithErrorChan calls the passed functions in a goroutine, limiting the number of goroutines running at the same time, 46 | // returns a chan of errors. 47 | func RestrictParallelWithErrorChan(concurrency int, fns ...func() error) chan error { 48 | total := len(fns) 49 | if concurrency <= 0 { 50 | concurrency = 1 51 | } 52 | if concurrency > total { 53 | concurrency = total 54 | } 55 | var wg sync.WaitGroup 56 | errs := make(chan error, total) 57 | jobs := make(chan func() error, concurrency) 58 | wg.Add(concurrency) 59 | for i := 0; i < concurrency; i++ { 60 | //consumer 61 | go func(jobs chan func() error, errs chan error) { 62 | defer wg.Done() 63 | for fn := range jobs { 64 | errs <- try(fn, nil) 65 | } 66 | }(jobs, errs) 67 | } 68 | go func(errs chan error) { 69 | //producer 70 | for _, fn := range fns { 71 | jobs <- fn 72 | } 73 | close(jobs) 74 | //wait for block errs 75 | wg.Wait() 76 | close(errs) 77 | }(errs) 78 | return errs 79 | } 80 | -------------------------------------------------------------------------------- /core/goroutine/serial.go: -------------------------------------------------------------------------------- 1 | package goroutine 2 | 3 | import ( 4 | "go.uber.org/multierr" 5 | ) 6 | 7 | // SerialWithError ... 8 | func SerialWithError(fns ...func() error) func() error { 9 | return func() error { 10 | var errs error 11 | for _, fn := range fns { 12 | errs = multierr.Append(errs, try(fn, nil)) 13 | } 14 | return errs 15 | } 16 | } 17 | 18 | // SerialUntilError 创建一个迭代器 19 | func SerialUntilError(fns ...func() error) func() error { 20 | return func() error { 21 | for _, fn := range fns { 22 | if err := try(fn, nil); err != nil { 23 | return err 24 | // return errors.Wrap(err, xstring.FunctionName(fn)) 25 | } 26 | } 27 | return nil 28 | } 29 | } 30 | 31 | // WhenError 策略注入 32 | type WhenError int 33 | 34 | // ReturnWhenError ... 35 | var ( 36 | 37 | // ReturnWhenError ... 38 | ReturnWhenError WhenError = 1 39 | 40 | // ContinueWhenError ... 41 | ContinueWhenError WhenError = 2 42 | 43 | // PanicWhenError ... 44 | PanicWhenError WhenError = 3 45 | 46 | // LastErrorWhenError ... 47 | LastErrorWhenError WhenError = 4 48 | ) 49 | 50 | // SerialWhenError ... 51 | func SerialWhenError(we WhenError) func(fn ...func() error) func() error { 52 | return func(fns ...func() error) func() error { 53 | return func() error { 54 | var errs error 55 | for _, fn := range fns { 56 | if err := try(fn, nil); err != nil { 57 | switch we { 58 | case ReturnWhenError: // 直接退出 59 | return err 60 | case ContinueWhenError: // 继续执行 61 | errs = multierr.Append(errs, err) 62 | case PanicWhenError: // panic 63 | panic(err) 64 | case LastErrorWhenError: // 返回最后一个错误 65 | errs = err 66 | } 67 | } 68 | } 69 | return errs 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/hooks/hooks.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | var ( 10 | globalHooks = make([][]func(), StageMax) 11 | ) 12 | 13 | type Stage int 14 | 15 | func (s Stage) String() string { 16 | switch s { 17 | case Stage_BeforeLoadConfig: 18 | return "BeforeLoadConfig" 19 | case Stage_AfterLoadConfig: 20 | return "AfterLoadConfig" 21 | case Stage_BeforeRun: 22 | return "BeforeRun" 23 | case Stage_BeforeStop: 24 | return "BeforeStop" 25 | case Stage_AfterStop: 26 | return "AfterStop" 27 | } 28 | 29 | return "Unknown" 30 | } 31 | 32 | const ( 33 | Stage_BeforeLoadConfig Stage = iota 34 | Stage_AfterLoadConfig 35 | Stage_BeforeRun 36 | Stage_BeforeStop 37 | Stage_AfterStop 38 | StageMax 39 | ) 40 | 41 | // Register 注册一个defer函数 42 | func Register(stage Stage, fns ...func()) { 43 | globalHooks[stage] = append(globalHooks[stage], fns...) 44 | } 45 | 46 | // Do 执行 47 | func Do(stage Stage) { 48 | fmt.Printf("[ratel] %+v\n", color.GreenString(fmt.Sprintf("hook stage (%s)...", stage))) 49 | 50 | if stage >= StageMax { 51 | return 52 | } 53 | 54 | for i := len(globalHooks[stage]) - 1; i >= 0; i-- { 55 | fn := globalHooks[stage][i] 56 | if fn != nil { 57 | fn() 58 | } 59 | } 60 | 61 | fmt.Printf("[ratel] %+v\n", color.GreenString(fmt.Sprintf("hook stage (%s)... done", stage))) 62 | } 63 | -------------------------------------------------------------------------------- /core/hostname/hostname.go: -------------------------------------------------------------------------------- 1 | package hostname 2 | 3 | import "os" 4 | 5 | func Hostname() string { 6 | // Get the hostname 7 | hostName, err := os.Hostname() 8 | if err != nil { 9 | hostName = "unknown" 10 | } 11 | return hostName 12 | } 13 | -------------------------------------------------------------------------------- /core/logger/entry/entry.go: -------------------------------------------------------------------------------- 1 | package entry 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Entry ... 8 | type Entry struct { 9 | Host string `json:"host"` 10 | Timestamp time.Time `json:"timestamp"` 11 | File string `json:"file"` 12 | Func string `json:"func"` 13 | Message string `json:"message"` 14 | Data map[string]any `json:"data"` 15 | Level string `json:"level"` 16 | } 17 | -------------------------------------------------------------------------------- /core/logger/es/doc.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/stores/elasticsearch" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // IndexString ... 11 | func IndexString() string { 12 | body := `{ 13 | "settings": { 14 | "index": { 15 | "number_of_shards": 1, 16 | "number_of_replicas": 0 17 | } 18 | }, 19 | "mappings": { 20 | "dynamic": false, 21 | "properties": { 22 | "file": { 23 | "type": "keyword" 24 | }, 25 | "func": { 26 | "type": "keyword" 27 | }, 28 | "message": { 29 | "type": "keyword" 30 | }, 31 | "level": { 32 | "type": "keyword" 33 | }, 34 | "data": { 35 | "type": "object" 36 | }, 37 | "host": { 38 | "type": "keyword" 39 | }, 40 | "timestamp": { 41 | "type": "date" 42 | } 43 | } 44 | } 45 | }` 46 | return body 47 | } 48 | 49 | // CreateIndex ... 50 | func CreateIndex(client *elasticsearch.Client) error { 51 | ctx := context.Background() 52 | //check es index 53 | exists, err := client.IndexExists("logger_entry").Do(ctx) 54 | if err != nil { 55 | return err 56 | } 57 | if !exists { 58 | // Create a new index. 59 | createIndex, err := client.CreateIndex("logger_entry").BodyJson(IndexString()).Do(ctx) 60 | if err != nil { 61 | return err 62 | } 63 | if !createIndex.Acknowledged { 64 | return errors.New("Not acknowledged") 65 | } 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /core/logger/es/exec.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/core/logger/entry" 7 | "github.com/abulo/ratel/v3/stores/elasticsearch" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // ExecCloser 将logrus条目写入数据库并关闭数据库 12 | type ExecCloser interface { 13 | Exec(entry *entry.Entry) error 14 | } 15 | 16 | type defaultExec struct { 17 | client *elasticsearch.Client 18 | index string 19 | canClose bool 20 | } 21 | 22 | // NewExec create an exec instance 23 | func NewExec(client *elasticsearch.Client, index string) ExecCloser { 24 | return &defaultExec{ 25 | client: client, 26 | index: index, 27 | canClose: true, 28 | } 29 | } 30 | 31 | // NewExecWithURL create an exec instance 32 | func NewExecWithURL(client *elasticsearch.Client, index string) ExecCloser { 33 | return &defaultExec{ 34 | client: client, 35 | index: index, 36 | canClose: true, 37 | } 38 | } 39 | 40 | // Exec ... 41 | func (e *defaultExec) Exec(entry *entry.Entry) error { 42 | ctx := context.Background() 43 | _, err := e.client.Index().Index(e.index).Id(uuid.New().String()).BodyJson(entry).Do(ctx) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /core/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | // Logger 日志组件 8 | var ( 9 | Logger *logrus.Logger = logrus.New() 10 | LoggerEntry *logrus.Entry = logrus.NewEntry(Logger) 11 | ) 12 | -------------------------------------------------------------------------------- /core/logger/mongo/exec.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/core/logger/entry" 7 | "github.com/abulo/ratel/v3/stores/mongodb" 8 | "go.mongodb.org/mongo-driver/bson" 9 | ) 10 | 11 | // ExecCloser 将logrus条目写入数据库并关闭数据库 12 | type ExecCloser interface { 13 | Exec(entry *entry.Entry) error 14 | } 15 | 16 | type defaultExec struct { 17 | client *mongodb.MongoDB 18 | collection string 19 | canClose bool 20 | } 21 | 22 | // NewExec create an exec instance 23 | func NewExec(client *mongodb.MongoDB, collection string) ExecCloser { 24 | return &defaultExec{ 25 | client: client, 26 | collection: collection, 27 | canClose: true, 28 | } 29 | } 30 | 31 | // NewExecWithURL create an exec instance 32 | func NewExecWithURL(client *mongodb.MongoDB, collection string) ExecCloser { 33 | return &defaultExec{ 34 | client: client, 35 | collection: collection, 36 | canClose: true, 37 | } 38 | } 39 | 40 | // Exec ... 41 | func (e *defaultExec) Exec(entry *entry.Entry) error { 42 | item := make(bson.M) 43 | item["host"] = entry.Host 44 | item["timestamp"] = entry.Timestamp 45 | item["file"] = entry.File 46 | item["func"] = entry.Func 47 | item["message"] = entry.Message 48 | item["level"] = entry.Level 49 | data := bson.M(entry.Data) 50 | item["data"] = data 51 | ctx := context.Background() 52 | handler, err := e.client.NewCollection(e.collection) 53 | if err != nil { 54 | return err 55 | } 56 | _, err = handler.InsertOne(ctx, item) 57 | return err 58 | } 59 | -------------------------------------------------------------------------------- /core/logger/queue/chan_queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // NewQueue 创建一个队列,指定缓冲区数和工作线程数 9 | func NewQueue(maxCapacity, maxThread int) *Queue { 10 | return &Queue{ 11 | jobQueue: make(chan Jober, maxCapacity), 12 | maxWorkers: maxThread, 13 | workerPool: make(chan chan Jober, maxThread), 14 | workers: make([]*worker, maxThread), 15 | wg: new(sync.WaitGroup), 16 | } 17 | } 18 | 19 | // Queue 任务队列排队以在高并发情况下减轻服务器压力并改进任务处理 20 | type Queue struct { 21 | maxWorkers int 22 | jobQueue chan Jober 23 | workerPool chan chan Jober 24 | workers []*worker 25 | running uint32 26 | wg *sync.WaitGroup 27 | } 28 | 29 | // Run 开始运行队列 30 | func (q *Queue) Run() { 31 | if atomic.LoadUint32(&q.running) == 1 { 32 | return 33 | } 34 | 35 | atomic.StoreUint32(&q.running, 1) 36 | for i := 0; i < q.maxWorkers; i++ { 37 | q.workers[i] = newWorker(q.workerPool, q.wg) 38 | q.workers[i].Start() 39 | } 40 | 41 | go q.dispatcher() 42 | } 43 | 44 | func (q *Queue) dispatcher() { 45 | for job := range q.jobQueue { 46 | worker := <-q.workerPool 47 | worker <- job 48 | } 49 | } 50 | 51 | // Terminate 终止队列以接收任务并释放资源 52 | func (q *Queue) Terminate() { 53 | if atomic.LoadUint32(&q.running) != 1 { 54 | return 55 | } 56 | 57 | atomic.StoreUint32(&q.running, 0) 58 | q.wg.Wait() 59 | 60 | close(q.jobQueue) 61 | for i := 0; i < q.maxWorkers; i++ { 62 | q.workers[i].Stop() 63 | } 64 | close(q.workerPool) 65 | } 66 | 67 | // Push 将可执行任务放入队列 68 | func (q *Queue) Push(job Jober) { 69 | if atomic.LoadUint32(&q.running) != 1 { 70 | return 71 | } 72 | 73 | q.wg.Add(1) 74 | q.jobQueue <- job 75 | } 76 | -------------------------------------------------------------------------------- /core/logger/queue/chan_queue_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync/atomic" 7 | "testing" 8 | ) 9 | 10 | func TestQueue(t *testing.T) { 11 | q := NewQueue(2, 10) 12 | q.Run() 13 | 14 | var count int64 15 | 16 | for i := 0; i < 10; i++ { 17 | job := NewJob("foo", func(v any) { 18 | atomic.AddInt64(&count, 1) 19 | }) 20 | q.Push(job) 21 | } 22 | 23 | q.Terminate() 24 | 25 | if count != 10 { 26 | t.Error(count) 27 | } 28 | } 29 | 30 | func TestSyncQueue(t *testing.T) { 31 | q := NewQueue(1, 2) 32 | q.Run() 33 | defer q.Terminate() 34 | 35 | sjob := NewSyncJob("foo", func(v any) (any, error) { 36 | return fmt.Sprintf("%s_bar", v), nil 37 | }) 38 | q.Push(sjob) 39 | 40 | result := <-sjob.Wait() 41 | if err := sjob.Error(); err != nil { 42 | t.Error(err.Error()) 43 | } 44 | 45 | if !reflect.DeepEqual(result, "foo_bar") { 46 | t.Error(result) 47 | } 48 | } 49 | 50 | func ExampleQueue() { 51 | q := NewQueue(1, 10) 52 | q.Run() 53 | 54 | var count int64 55 | 56 | for i := 0; i < 10; i++ { 57 | job := NewJob("foo", func(v any) { 58 | atomic.AddInt64(&count, 1) 59 | }) 60 | q.Push(job) 61 | } 62 | 63 | q.Terminate() 64 | fmt.Println(count) 65 | // output: 10 66 | } 67 | 68 | func BenchmarkQueue(b *testing.B) { 69 | q := NewQueue(10, 100) 70 | q.Run() 71 | 72 | b.ResetTimer() 73 | b.RunParallel(func(pb *testing.PB) { 74 | for pb.Next() { 75 | job := NewJob("", func(v any) { 76 | _ = v 77 | }) 78 | q.Push(job) 79 | } 80 | }) 81 | q.Terminate() 82 | } 83 | -------------------------------------------------------------------------------- /core/logger/queue/job.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | // Jober 是一个可以执行的异步任务 4 | type Jober interface { 5 | Job() 6 | } 7 | 8 | // SyncJober 可以执行的同步任务 9 | type SyncJober interface { 10 | Jober 11 | Wait() <-chan any 12 | Error() error 13 | } 14 | 15 | type job struct { 16 | v any 17 | callback func(any) 18 | } 19 | 20 | // NewJob 创建一个异步任务 21 | func NewJob(v any, fn func(any)) Jober { 22 | return &job{ 23 | v: v, 24 | callback: fn, 25 | } 26 | } 27 | 28 | // Job ... 29 | func (j *job) Job() { 30 | j.callback(j.v) 31 | } 32 | 33 | type syncJob struct { 34 | err error 35 | result chan any 36 | v any 37 | callback func(any) (any, error) 38 | } 39 | 40 | // NewSyncJob 创建同步任务 41 | func NewSyncJob(v any, fn func(any) (any, error)) SyncJober { 42 | return &syncJob{ 43 | result: make(chan any, 1), 44 | v: v, 45 | callback: fn, 46 | } 47 | } 48 | 49 | // Job ... 50 | func (j *syncJob) Job() { 51 | result, err := j.callback(j.v) 52 | if err != nil { 53 | j.err = err 54 | close(j.result) 55 | return 56 | } 57 | 58 | j.result <- result 59 | 60 | close(j.result) 61 | } 62 | 63 | // Wait ... 64 | func (j *syncJob) Wait() <-chan any { 65 | return j.result 66 | } 67 | 68 | // Error ... 69 | func (j *syncJob) Error() error { 70 | return j.err 71 | } 72 | -------------------------------------------------------------------------------- /core/logger/queue/job_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func TestJob(t *testing.T) { 13 | var ( 14 | result any 15 | wg sync.WaitGroup 16 | ) 17 | 18 | wg.Add(1) 19 | job := NewJob("foo", func(v any) { 20 | result = fmt.Sprintf("%s_bar", v) 21 | wg.Done() 22 | }) 23 | 24 | go job.Job() 25 | wg.Wait() 26 | 27 | if !reflect.DeepEqual(result, "foo_bar") { 28 | t.Error(result) 29 | } 30 | } 31 | 32 | func TestSyncJob(t *testing.T) { 33 | sjob := NewSyncJob("foo", func(v any) (any, error) { 34 | return fmt.Sprintf("%s_bar", v), nil 35 | }) 36 | 37 | go sjob.Job() 38 | 39 | result := <-sjob.Wait() 40 | if err := sjob.Error(); err != nil { 41 | t.Error(err.Error()) 42 | return 43 | } 44 | 45 | if !reflect.DeepEqual(result, "foo_bar") { 46 | t.Error(result) 47 | } 48 | } 49 | 50 | func TestSyncJobError(t *testing.T) { 51 | sjob := NewSyncJob("foo", func(v any) (any, error) { 52 | return nil, errors.New("mock error") 53 | }) 54 | 55 | go sjob.Job() 56 | 57 | result := <-sjob.Wait() 58 | if err := sjob.Error(); err == nil { 59 | t.Error("mock error") 60 | return 61 | } 62 | 63 | if result != nil { 64 | t.Error("result is nil") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/logger/queue/list_queue_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "testing" 7 | ) 8 | 9 | type testJob struct { 10 | payload int 11 | callback func(int) 12 | } 13 | 14 | func (t *testJob) Job() { 15 | t.payload++ 16 | t.callback(t.payload) 17 | } 18 | 19 | func TestListQueue(t *testing.T) { 20 | q := NewListQueue(2) 21 | q.Run() 22 | 23 | var data int64 24 | q.Push(&testJob{ 25 | payload: 0, 26 | callback: func(result int) { 27 | atomic.AddInt64(&data, int64(result)) 28 | }, 29 | }) 30 | q.Push(&testJob{ 31 | payload: 0, 32 | callback: func(result int) { 33 | atomic.AddInt64(&data, int64(result)) 34 | }, 35 | }) 36 | q.Push(&testJob{ 37 | payload: 0, 38 | callback: func(result int) { 39 | atomic.AddInt64(&data, int64(result)) 40 | }, 41 | }) 42 | q.Terminate() 43 | if data != 3 { 44 | t.Error(data) 45 | } 46 | } 47 | 48 | func ExampleListQueue() { 49 | q := NewListQueue(10) 50 | q.Run() 51 | 52 | var count int64 53 | for i := 0; i < 10; i++ { 54 | job := NewJob("foo", func(v any) { 55 | atomic.AddInt64(&count, 1) 56 | }) 57 | q.Push(job) 58 | } 59 | 60 | q.Terminate() 61 | fmt.Println(count) 62 | // output: 10 63 | } 64 | 65 | func BenchmarkListQueue(b *testing.B) { 66 | q := NewListQueue(100) 67 | q.Run() 68 | 69 | b.ResetTimer() 70 | b.RunParallel(func(pb *testing.PB) { 71 | for pb.Next() { 72 | job := NewJob("", func(v any) { 73 | _ = v 74 | }) 75 | q.Push(job) 76 | } 77 | }) 78 | q.Terminate() 79 | } 80 | -------------------------------------------------------------------------------- /core/logger/queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | var ( 4 | internalQueue Queuer 5 | ) 6 | 7 | // Queuer 是一个任务队列,用于在高并发情况下缓解服务器压力并改进任务处理 8 | type Queuer interface { 9 | Run() 10 | Push(job Jober) 11 | Terminate() 12 | } 13 | 14 | // Run 运行start running queue,指定缓冲区数和工作线程数 15 | func Run(maxCapacity, maxThread int) { 16 | if internalQueue == nil { 17 | internalQueue = NewQueue(maxCapacity, maxThread) 18 | } 19 | internalQueue.Run() 20 | } 21 | 22 | // RunListQueue 启动运行列表队列,指定工作线程数 23 | func RunListQueue(maxThread int) { 24 | if internalQueue == nil { 25 | internalQueue = NewListQueue(maxThread) 26 | } 27 | internalQueue.Run() 28 | } 29 | 30 | // Push 将可执行任务推入队列 31 | func Push(job Jober) { 32 | if internalQueue == nil { 33 | return 34 | } 35 | internalQueue.Push(job) 36 | } 37 | 38 | // Terminate 终止队列以接收任务并释放资源 39 | func Terminate() { 40 | if internalQueue == nil { 41 | return 42 | } 43 | internalQueue.Terminate() 44 | } 45 | -------------------------------------------------------------------------------- /core/logger/queue/worker.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // 创建一个worker队列 8 | func newWorker(pool chan chan Jober, wg *sync.WaitGroup) *worker { 9 | return &worker{ 10 | pool: pool, 11 | wg: wg, 12 | jobChan: make(chan Jober), 13 | quit: make(chan struct{}), 14 | } 15 | } 16 | 17 | // worker队列结构体 18 | type worker struct { 19 | pool chan chan Jober 20 | wg *sync.WaitGroup 21 | jobChan chan Jober 22 | quit chan struct{} 23 | } 24 | 25 | // Start 开始这个worker 26 | func (w *worker) Start() { 27 | w.pool <- w.jobChan 28 | go w.dispatcher() 29 | } 30 | 31 | // 分发执行 32 | func (w *worker) dispatcher() { 33 | for { 34 | select { 35 | case j := <-w.jobChan: 36 | j.Job() 37 | w.pool <- w.jobChan 38 | w.wg.Done() 39 | case <-w.quit: 40 | <-w.pool 41 | close(w.jobChan) 42 | return 43 | } 44 | } 45 | } 46 | 47 | // Stop 停止 这个worker 48 | func (w *worker) Stop() { 49 | close(w.quit) 50 | } 51 | -------------------------------------------------------------------------------- /core/logger/sql/exec.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/abulo/ratel/v3/core/logger/entry" 7 | "github.com/abulo/ratel/v3/stores/null" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // ExecCloser 将logrus条目写入数据库并关闭数据库 12 | type ExecCloser interface { 13 | Exec(entry *entry.Entry) error 14 | } 15 | 16 | var tableName string 17 | 18 | type defaultExec struct { 19 | client *gorm.DB 20 | canClose bool 21 | } 22 | 23 | // NewExec create an exec instance 24 | func NewExec(client *gorm.DB, loggerTableName string) ExecCloser { 25 | tableName = loggerTableName 26 | return &defaultExec{ 27 | client: client, 28 | canClose: true, 29 | } 30 | } 31 | 32 | // NewExecWithURL create an exec instance 33 | func NewExecWithURL(client *gorm.DB, loggerTableName string) ExecCloser { 34 | tableName = loggerTableName 35 | return &defaultExec{ 36 | client: client, 37 | canClose: true, 38 | } 39 | } 40 | 41 | func (e *defaultExec) Exec(entry *entry.Entry) error { 42 | daoItem := &Dao{} 43 | daoItem.Host = null.StringFrom(entry.Host) 44 | daoItem.File = null.StringFrom(entry.File) 45 | daoItem.Func = null.StringFrom(entry.Func) 46 | daoItem.Message = null.StringFrom(entry.Message) 47 | daoItem.Level = null.StringFrom(entry.Level) 48 | daoItem.Timestamp = null.TimeStampFrom(entry.Timestamp) 49 | data, _ := json.Marshal(entry.Data) 50 | daoItem.Data = null.JSONFrom(data) 51 | return e.client.Create(daoItem).Error 52 | } 53 | 54 | type Dao struct { 55 | Id *int64 `gorm:"primaryKey;autoIncrement;column:id" json:"id"` 56 | Host null.String `gorm:"column:host" json:"host"` 57 | Timestamp null.TimeStamp `gorm:"column:timestamp" json:"timestamp"` 58 | File null.String `gorm:"column:file" json:"file"` 59 | Func null.String `gorm:"column:func" json:"func"` 60 | Message null.String `gorm:"column:message" json:"message"` 61 | Level null.String `gorm:"column:level" json:"level"` 62 | Data null.JSON `gorm:"column:data" json:"data"` 63 | } 64 | 65 | // 表名重写为 tableName 66 | func (Dao) TableName() string { 67 | return tableName 68 | } 69 | -------------------------------------------------------------------------------- /core/metric/counter.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/abulo/ratel/v3/core/constant" 5 | "github.com/prometheus/client_golang/prometheus" 6 | ) 7 | 8 | // CounterVecOpts ... 9 | type CounterVecOpts struct { 10 | Namespace string 11 | Subsystem string 12 | Name string 13 | Help string 14 | Labels []string 15 | } 16 | 17 | // Build ... 18 | func (opts CounterVecOpts) Build() *counterVec { 19 | vec := prometheus.NewCounterVec( 20 | prometheus.CounterOpts{ 21 | Namespace: opts.Namespace, 22 | Subsystem: opts.Subsystem, 23 | Name: opts.Name, 24 | Help: opts.Help, 25 | }, opts.Labels) 26 | prometheus.MustRegister(vec) 27 | return &counterVec{ 28 | CounterVec: vec, 29 | } 30 | } 31 | 32 | // NewCounterVec ... 33 | func NewCounterVec(name string, labels []string) *counterVec { 34 | return CounterVecOpts{ 35 | Namespace: constant.DefaultNamespace, 36 | Name: name, 37 | Help: name, 38 | Labels: labels, 39 | }.Build() 40 | } 41 | 42 | type counterVec struct { 43 | *prometheus.CounterVec 44 | } 45 | 46 | // Inc ... 47 | func (counter *counterVec) Inc(labels ...string) { 48 | counter.WithLabelValues(labels...).Inc() 49 | } 50 | 51 | // Add ... 52 | func (counter *counterVec) Add(v float64, labels ...string) { 53 | counter.WithLabelValues(labels...).Add(v) 54 | } 55 | -------------------------------------------------------------------------------- /core/metric/counter_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestCounterVecOpts_Build(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | opts CounterVecOpts 12 | want *counterVec 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := tt.opts.Build(); !reflect.DeepEqual(got, tt.want) { 19 | t.Errorf("CounterVecOpts.Build() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func TestNewCounterVec(t *testing.T) { 26 | type args struct { 27 | name string 28 | labels []string 29 | } 30 | tests := []struct { 31 | name string 32 | args args 33 | want *counterVec 34 | }{ 35 | // TODO: Add test cases. 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := NewCounterVec(tt.args.name, tt.args.labels); !reflect.DeepEqual(got, tt.want) { 40 | t.Errorf("NewCounterVec() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func Test_counterVec_Inc(t *testing.T) { 47 | type args struct { 48 | labels []string 49 | } 50 | tests := []struct { 51 | name string 52 | counter *counterVec 53 | args args 54 | }{ 55 | // TODO: Add test cases. 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | tt.counter.Inc(tt.args.labels...) 60 | }) 61 | } 62 | } 63 | 64 | func Test_counterVec_Add(t *testing.T) { 65 | type args struct { 66 | v float64 67 | labels []string 68 | } 69 | tests := []struct { 70 | name string 71 | counter *counterVec 72 | args args 73 | }{ 74 | // TODO: Add test cases. 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | tt.counter.Add(tt.args.v, tt.args.labels...) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/metric/gauge.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/abulo/ratel/v3/core/constant" 5 | "github.com/prometheus/client_golang/prometheus" 6 | ) 7 | 8 | // GaugeVecOpts ... 9 | type GaugeVecOpts struct { 10 | Namespace string 11 | Subsystem string 12 | Name string 13 | Help string 14 | Labels []string 15 | } 16 | 17 | type gaugeVec struct { 18 | *prometheus.GaugeVec 19 | } 20 | 21 | // Build ... 22 | func (opts GaugeVecOpts) Build() *gaugeVec { 23 | vec := prometheus.NewGaugeVec( 24 | prometheus.GaugeOpts{ 25 | Namespace: opts.Namespace, 26 | Subsystem: opts.Subsystem, 27 | Name: opts.Name, 28 | Help: opts.Help, 29 | }, opts.Labels) 30 | prometheus.MustRegister(vec) 31 | return &gaugeVec{ 32 | GaugeVec: vec, 33 | } 34 | } 35 | 36 | // NewGaugeVec ... 37 | func NewGaugeVec(name string, labels []string) *gaugeVec { 38 | return GaugeVecOpts{ 39 | Namespace: constant.DefaultNamespace, 40 | Name: name, 41 | Help: name, 42 | Labels: labels, 43 | }.Build() 44 | } 45 | 46 | // Inc ... 47 | func (gv *gaugeVec) Inc(labels ...string) { 48 | gv.WithLabelValues(labels...).Inc() 49 | } 50 | 51 | // Add ... 52 | func (gv *gaugeVec) Add(v float64, labels ...string) { 53 | gv.WithLabelValues(labels...).Add(v) 54 | } 55 | 56 | // Set ... 57 | func (gv *gaugeVec) Set(v float64, labels ...string) { 58 | gv.WithLabelValues(labels...).Set(v) 59 | } 60 | -------------------------------------------------------------------------------- /core/metric/gauge_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGaugeVecOpts_Build(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | opts GaugeVecOpts 12 | want *gaugeVec 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := tt.opts.Build(); !reflect.DeepEqual(got, tt.want) { 19 | t.Errorf("GaugeVecOpts.Build() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func TestNewGaugeVec(t *testing.T) { 26 | type args struct { 27 | name string 28 | labels []string 29 | } 30 | tests := []struct { 31 | name string 32 | args args 33 | want *gaugeVec 34 | }{ 35 | // TODO: Add test cases. 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := NewGaugeVec(tt.args.name, tt.args.labels); !reflect.DeepEqual(got, tt.want) { 40 | t.Errorf("NewGaugeVec() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func Test_gaugeVec_Inc(t *testing.T) { 47 | type args struct { 48 | labels []string 49 | } 50 | tests := []struct { 51 | name string 52 | gv *gaugeVec 53 | args args 54 | }{ 55 | // TODO: Add test cases. 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | tt.gv.Inc(tt.args.labels...) 60 | }) 61 | } 62 | } 63 | 64 | func Test_gaugeVec_Add(t *testing.T) { 65 | type args struct { 66 | v float64 67 | labels []string 68 | } 69 | tests := []struct { 70 | name string 71 | gv *gaugeVec 72 | args args 73 | }{ 74 | // TODO: Add test cases. 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | tt.gv.Add(tt.args.v, tt.args.labels...) 79 | }) 80 | } 81 | } 82 | 83 | func Test_gaugeVec_Set(t *testing.T) { 84 | type args struct { 85 | v float64 86 | labels []string 87 | } 88 | tests := []struct { 89 | name string 90 | gv *gaugeVec 91 | args args 92 | }{ 93 | // TODO: Add test cases. 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | tt.gv.Set(tt.args.v, tt.args.labels...) 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/metric/histogram.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | // HistogramVecOpts ... 6 | type HistogramVecOpts struct { 7 | Namespace string 8 | Subsystem string 9 | Name string 10 | Help string 11 | Labels []string 12 | Buckets []float64 13 | } 14 | 15 | type histogramVec struct { 16 | *prometheus.HistogramVec 17 | } 18 | 19 | // Build ... 20 | func (opts HistogramVecOpts) Build() *histogramVec { 21 | vec := prometheus.NewHistogramVec( 22 | prometheus.HistogramOpts{ 23 | Namespace: opts.Namespace, 24 | Subsystem: opts.Subsystem, 25 | Name: opts.Name, 26 | Help: opts.Help, 27 | Buckets: opts.Buckets, 28 | }, opts.Labels) 29 | prometheus.MustRegister(vec) 30 | return &histogramVec{ 31 | HistogramVec: vec, 32 | } 33 | } 34 | 35 | // Observe ... 36 | func (histogram *histogramVec) Observe(v float64, labels ...string) { 37 | histogram.WithLabelValues(labels...).Observe(v) 38 | } 39 | -------------------------------------------------------------------------------- /core/metric/histogram_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestHistogramVecOpts_Build(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | opts HistogramVecOpts 12 | want *histogramVec 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := tt.opts.Build(); !reflect.DeepEqual(got, tt.want) { 19 | t.Errorf("HistogramVecOpts.Build() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func Test_histogramVec_Observe(t *testing.T) { 26 | type args struct { 27 | v float64 28 | labels []string 29 | } 30 | tests := []struct { 31 | name string 32 | histogram *histogramVec 33 | args args 34 | }{ 35 | // TODO: Add test cases. 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | tt.histogram.Observe(tt.args.v, tt.args.labels...) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/metric/summary.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | // SummaryVecOpts ... 6 | type SummaryVecOpts struct { 7 | Namespace string 8 | Subsystem string 9 | Name string 10 | Help string 11 | Labels []string 12 | } 13 | 14 | type summaryVec struct { 15 | *prometheus.SummaryVec 16 | } 17 | 18 | // Build ... 19 | func (opts SummaryVecOpts) Build() *summaryVec { 20 | vec := prometheus.NewSummaryVec( 21 | prometheus.SummaryOpts{ 22 | Namespace: opts.Namespace, 23 | Subsystem: opts.Subsystem, 24 | Name: opts.Name, 25 | Help: opts.Help, 26 | }, opts.Labels) 27 | prometheus.MustRegister(vec) 28 | return &summaryVec{ 29 | SummaryVec: vec, 30 | } 31 | } 32 | 33 | // Observe ... 34 | func (summary *summaryVec) Observe(v float64, labels ...string) { 35 | summary.WithLabelValues(labels...).Observe(v) 36 | } 37 | -------------------------------------------------------------------------------- /core/metric/summary_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSummaryVecOpts_Build(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | opts SummaryVecOpts 12 | want *summaryVec 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := tt.opts.Build(); !reflect.DeepEqual(got, tt.want) { 19 | t.Errorf("SummaryVecOpts.Build() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func Test_summaryVec_Observe(t *testing.T) { 26 | type args struct { 27 | v float64 28 | labels []string 29 | } 30 | tests := []struct { 31 | name string 32 | summary *summaryVec 33 | args args 34 | }{ 35 | // TODO: Add test cases. 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | tt.summary.Observe(tt.args.v, tt.args.labels...) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/ratelimit/reader.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import "io" 4 | 5 | type reader struct { 6 | r io.Reader 7 | bucket *Bucket 8 | } 9 | 10 | // Reader returns a reader that is rate limited by 11 | // the given token bucket. Each token in the bucket 12 | // represents one byte. 13 | func Reader(r io.Reader, bucket *Bucket) io.Reader { 14 | return &reader{ 15 | r: r, 16 | bucket: bucket, 17 | } 18 | } 19 | 20 | // Read ... 21 | func (r *reader) Read(buf []byte) (int, error) { 22 | n, err := r.r.Read(buf) 23 | if n <= 0 { 24 | return n, err 25 | } 26 | r.bucket.Wait(int64(n)) 27 | return n, err 28 | } 29 | 30 | type writer struct { 31 | w io.Writer 32 | bucket *Bucket 33 | } 34 | 35 | // Writer returns a reader that is rate limited by 36 | // the given token bucket. Each token in the bucket 37 | // represents one byte. 38 | func Writer(w io.Writer, bucket *Bucket) io.Writer { 39 | return &writer{ 40 | w: w, 41 | bucket: bucket, 42 | } 43 | } 44 | 45 | // Write ... 46 | func (w *writer) Write(buf []byte) (int, error) { 47 | w.bucket.Wait(int64(len(buf))) 48 | return w.w.Write(buf) 49 | } 50 | -------------------------------------------------------------------------------- /core/ratelimit/reader_test.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestReader(t *testing.T) { 10 | type args struct { 11 | r io.Reader 12 | bucket *Bucket 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want io.Reader 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if got := Reader(tt.args.r, tt.args.bucket); !reflect.DeepEqual(got, tt.want) { 24 | t.Errorf("Reader() = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func Test_reader_Read(t *testing.T) { 31 | type args struct { 32 | buf []byte 33 | } 34 | tests := []struct { 35 | name string 36 | r *reader 37 | args args 38 | want int 39 | wantErr bool 40 | }{ 41 | // TODO: Add test cases. 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | got, err := tt.r.Read(tt.args.buf) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("reader.Read() error = %v, wantErr %v", err, tt.wantErr) 48 | return 49 | } 50 | if got != tt.want { 51 | t.Errorf("reader.Read() = %v, want %v", got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func Test_writer_Write(t *testing.T) { 58 | type args struct { 59 | buf []byte 60 | } 61 | tests := []struct { 62 | name string 63 | w *writer 64 | args args 65 | want int 66 | wantErr bool 67 | }{ 68 | // TODO: Add test cases. 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | got, err := tt.w.Write(tt.args.buf) 73 | if (err != nil) != tt.wantErr { 74 | t.Errorf("writer.Write() error = %v, wantErr %v", err, tt.wantErr) 75 | return 76 | } 77 | if got != tt.want { 78 | t.Errorf("writer.Write() = %v, want %v", got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /core/ratelimit/readme.md: -------------------------------------------------------------------------------- 1 | rl := ratelimit.NewLeakyBucket(time.Second, 15) // per second 2 | rl.TakeAvailable() 3 | 4 | rl = ratelimit.NewTokenBucket(time.Microsecond, 15) // per Microsecond 5 | rl.TakeAvailable() -------------------------------------------------------------------------------- /core/resource/atomicduration.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | // An AtomicDuration is an implementation of atomic duration. 9 | type AtomicDuration int64 10 | 11 | // NewAtomicDuration returns an AtomicDuration. 12 | func NewAtomicDuration() *AtomicDuration { 13 | return new(AtomicDuration) 14 | } 15 | 16 | // ForAtomicDuration returns an AtomicDuration with given value. 17 | func ForAtomicDuration(val time.Duration) *AtomicDuration { 18 | d := NewAtomicDuration() 19 | d.Set(val) 20 | return d 21 | } 22 | 23 | // CompareAndSwap compares current value with old, if equals, set the value to val. 24 | func (d *AtomicDuration) CompareAndSwap(old, val time.Duration) bool { 25 | return atomic.CompareAndSwapInt64((*int64)(d), int64(old), int64(val)) 26 | } 27 | 28 | // Load loads the current duration. 29 | func (d *AtomicDuration) Load() time.Duration { 30 | return time.Duration(atomic.LoadInt64((*int64)(d))) 31 | } 32 | 33 | // Set sets the value to val. 34 | func (d *AtomicDuration) Set(val time.Duration) { 35 | atomic.StoreInt64((*int64)(d), int64(val)) 36 | } 37 | -------------------------------------------------------------------------------- /core/resource/batcherror.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "bytes" 4 | 5 | type ( 6 | // A BatchError is an error that can hold multiple errors. 7 | BatchError struct { 8 | errs errorArray 9 | } 10 | 11 | errorArray []error 12 | ) 13 | 14 | // Add adds errs to be, nil errors are ignored. 15 | func (be *BatchError) Add(errs ...error) { 16 | for _, err := range errs { 17 | if err != nil { 18 | be.errs = append(be.errs, err) 19 | } 20 | } 21 | } 22 | 23 | // Err returns an error that represents all errors. 24 | func (be *BatchError) Err() error { 25 | switch len(be.errs) { 26 | case 0: 27 | return nil 28 | case 1: 29 | return be.errs[0] 30 | default: 31 | return be.errs 32 | } 33 | } 34 | 35 | // NotNil checks if any error inside. 36 | func (be *BatchError) NotNil() bool { 37 | return len(be.errs) > 0 38 | } 39 | 40 | // Error returns a string that represents inside errors. 41 | func (ea errorArray) Error() string { 42 | var buf bytes.Buffer 43 | 44 | for i := range ea { 45 | if i > 0 { 46 | buf.WriteByte('\n') 47 | } 48 | buf.WriteString(ea[i].Error()) 49 | } 50 | 51 | return buf.String() 52 | } 53 | -------------------------------------------------------------------------------- /core/resource/breakers.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "sync" 4 | 5 | var ( 6 | lock sync.RWMutex 7 | breakers = make(map[string]Breaker) 8 | ) 9 | 10 | // Do calls Breaker.Do on the Breaker with given name. 11 | func Do(name string, req func() error) error { 12 | return do(name, func(b Breaker) error { 13 | return b.Do(req) 14 | }) 15 | } 16 | 17 | // DoWithAcceptable calls Breaker.DoWithAcceptable on the Breaker with given name. 18 | func DoWithAcceptable(name string, req func() error, acceptable Acceptable) error { 19 | return do(name, func(b Breaker) error { 20 | return b.DoWithAcceptable(req, acceptable) 21 | }) 22 | } 23 | 24 | // DoWithFallback calls Breaker.DoWithFallback on the Breaker with given name. 25 | func DoWithFallback(name string, req func() error, fallback func(err error) error) error { 26 | return do(name, func(b Breaker) error { 27 | return b.DoWithFallback(req, fallback) 28 | }) 29 | } 30 | 31 | // DoWithFallbackAcceptable calls Breaker.DoWithFallbackAcceptable on the Breaker with given name. 32 | func DoWithFallbackAcceptable(name string, req func() error, fallback func(err error) error, 33 | acceptable Acceptable) error { 34 | return do(name, func(b Breaker) error { 35 | return b.DoWithFallbackAcceptable(req, fallback, acceptable) 36 | }) 37 | } 38 | 39 | // GetBreaker returns the Breaker with the given name. 40 | func GetBreaker(name string) Breaker { 41 | lock.RLock() 42 | b, ok := breakers[name] 43 | lock.RUnlock() 44 | if ok { 45 | return b 46 | } 47 | 48 | lock.Lock() 49 | b, ok = breakers[name] 50 | if !ok { 51 | b = NewBreaker(WithName(name)) 52 | breakers[name] = b 53 | } 54 | lock.Unlock() 55 | 56 | return b 57 | } 58 | 59 | // NoBreakerFor disables the circuit breaker for the given name. 60 | func NoBreakerFor(name string) { 61 | lock.Lock() 62 | breakers[name] = newNoOpBreaker() 63 | lock.Unlock() 64 | } 65 | 66 | func do(name string, execute func(b Breaker) error) error { 67 | return execute(GetBreaker(name)) 68 | } 69 | -------------------------------------------------------------------------------- /core/resource/nopbreaker.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | const noOpBreakerName = "nopBreaker" 4 | 5 | type noOpBreaker struct{} 6 | 7 | func newNoOpBreaker() Breaker { 8 | return noOpBreaker{} 9 | } 10 | 11 | func (b noOpBreaker) Name() string { 12 | return noOpBreakerName 13 | } 14 | 15 | func (b noOpBreaker) Allow() (Promise, error) { 16 | return nopPromise{}, nil 17 | } 18 | 19 | func (b noOpBreaker) Do(req func() error) error { 20 | return req() 21 | } 22 | 23 | func (b noOpBreaker) DoWithAcceptable(req func() error, _ Acceptable) error { 24 | return req() 25 | } 26 | 27 | func (b noOpBreaker) DoWithFallback(req func() error, _ func(err error) error) error { 28 | return req() 29 | } 30 | 31 | func (b noOpBreaker) DoWithFallbackAcceptable(req func() error, _ func(err error) error, 32 | _ Acceptable) error { 33 | return req() 34 | } 35 | 36 | type nopPromise struct{} 37 | 38 | func (p nopPromise) Accept() { 39 | } 40 | 41 | func (p nopPromise) Reject(_ string) { 42 | } 43 | -------------------------------------------------------------------------------- /core/resource/singleflight.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import "sync" 4 | 5 | type ( 6 | // SingleFlight lets the concurrent calls with the same key to share the call result. 7 | // For example, A called F, before it's done, B called F. Then B would not execute F, 8 | // and shared the result returned by F which called by A. 9 | // The calls with the same key are dependent, concurrent calls share the returned values. 10 | // A ------->calls F with key<------------------->returns val 11 | // B --------------------->calls F with key------>returns val 12 | SingleFlight interface { 13 | Do(key string, fn func() (any, error)) (any, error) 14 | DoEx(key string, fn func() (any, error)) (any, bool, error) 15 | } 16 | 17 | call struct { 18 | wg sync.WaitGroup 19 | val any 20 | err error 21 | } 22 | 23 | flightGroup struct { 24 | calls map[string]*call 25 | lock sync.Mutex 26 | } 27 | ) 28 | 29 | // NewSingleFlight returns a SingleFlight. 30 | func NewSingleFlight() SingleFlight { 31 | return &flightGroup{ 32 | calls: make(map[string]*call), 33 | } 34 | } 35 | 36 | func (g *flightGroup) Do(key string, fn func() (any, error)) (any, error) { 37 | c, done := g.createCall(key) 38 | if done { 39 | return c.val, c.err 40 | } 41 | 42 | g.makeCall(c, key, fn) 43 | return c.val, c.err 44 | } 45 | 46 | func (g *flightGroup) DoEx(key string, fn func() (any, error)) (val any, fresh bool, err error) { 47 | c, done := g.createCall(key) 48 | if done { 49 | return c.val, false, c.err 50 | } 51 | 52 | g.makeCall(c, key, fn) 53 | return c.val, true, c.err 54 | } 55 | 56 | func (g *flightGroup) createCall(key string) (c *call, done bool) { 57 | g.lock.Lock() 58 | if c, ok := g.calls[key]; ok { 59 | g.lock.Unlock() 60 | c.wg.Wait() 61 | return c, true 62 | } 63 | 64 | c = new(call) 65 | c.wg.Add(1) 66 | g.calls[key] = c 67 | g.lock.Unlock() 68 | 69 | return c, false 70 | } 71 | 72 | func (g *flightGroup) makeCall(c *call, key string, fn func() (any, error)) { 73 | defer func() { 74 | g.lock.Lock() 75 | delete(g.calls, key) 76 | g.lock.Unlock() 77 | c.wg.Done() 78 | }() 79 | 80 | c.val, c.err = fn() 81 | } 82 | -------------------------------------------------------------------------------- /core/resource/sourcemanager.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // A ResourceManager is a manager that used to manage resources. 9 | type ResourceManager struct { 10 | resources map[string]io.Closer 11 | singleFlight SingleFlight 12 | lock sync.RWMutex 13 | } 14 | 15 | // NewResourceManager returns a ResourceManager. 16 | func NewResourceManager() *ResourceManager { 17 | return &ResourceManager{ 18 | resources: make(map[string]io.Closer), 19 | singleFlight: NewSingleFlight(), 20 | } 21 | } 22 | 23 | // Close closes the manager. 24 | // Don't use the ResourceManager after Close() called. 25 | func (manager *ResourceManager) Close() error { 26 | manager.lock.Lock() 27 | defer manager.lock.Unlock() 28 | 29 | var be BatchError 30 | for _, resource := range manager.resources { 31 | if err := resource.Close(); err != nil { 32 | be.Add(err) 33 | } 34 | } 35 | 36 | // release resources to avoid using it later 37 | manager.resources = nil 38 | 39 | return be.Err() 40 | } 41 | 42 | // GetResource returns the resource associated with given key. 43 | func (manager *ResourceManager) GetResource(key string, create func() (io.Closer, error)) (io.Closer, error) { 44 | val, err := manager.singleFlight.Do(key, func() (any, error) { 45 | manager.lock.RLock() 46 | resource, ok := manager.resources[key] 47 | manager.lock.RUnlock() 48 | if ok { 49 | return resource, nil 50 | } 51 | 52 | resource, err := create() 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | manager.lock.Lock() 58 | defer manager.lock.Unlock() 59 | manager.resources[key] = resource 60 | 61 | return resource, nil 62 | }) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return val.(io.Closer), nil 68 | } 69 | 70 | // Inject injects the resource associated with given key. 71 | func (manager *ResourceManager) Inject(key string, resource io.Closer) { 72 | manager.lock.Lock() 73 | manager.resources[key] = resource 74 | manager.lock.Unlock() 75 | } 76 | -------------------------------------------------------------------------------- /core/singleton/singleton.go: -------------------------------------------------------------------------------- 1 | package singleton 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/abulo/ratel/v3/core/constant" 7 | "github.com/spf13/cast" 8 | ) 9 | 10 | var singleton sync.Map 11 | 12 | func genkey(module constant.Module, key string) string { 13 | return cast.ToString(int(module)) + key 14 | } 15 | 16 | func Load(module constant.Module, key string) (any, bool) { 17 | return singleton.Load(genkey(module, key)) 18 | } 19 | 20 | func Store(module constant.Module, key string, val any) { 21 | singleton.Store(genkey(module, key), val) 22 | } 23 | -------------------------------------------------------------------------------- /core/stack/defer.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | var ( 4 | globalDefers = NewStack() 5 | ) 6 | 7 | // Register 注册一个defer函数 8 | func Register(fns ...func() error) { 9 | globalDefers.Push(fns...) 10 | } 11 | 12 | // Clean 清除 13 | func Clean() { 14 | globalDefers.Clean() 15 | } 16 | -------------------------------------------------------------------------------- /core/stack/defer_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRegister(t *testing.T) { 10 | var str string 11 | type args struct { 12 | fns []func() error 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | }{ 18 | // TODO: Add test cases. 19 | { 20 | name: "register", 21 | args: args{ 22 | fns: []func() error{ 23 | func() error { str += "1,"; return nil }, 24 | func() error { str += "2,"; return nil }, 25 | func() error { str += "3,"; return nil }, 26 | func() error { str += "4,"; return nil }, 27 | nil, 28 | }, 29 | }, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | Register(tt.args.fns...) 35 | }) 36 | } 37 | } 38 | 39 | func TestClean(t *testing.T) { 40 | var str string 41 | globalDefers = NewStack() 42 | globalDefers.Push( 43 | func() error { str += "1,"; return nil }, 44 | func() error { str += "2,"; return nil }, 45 | func() error { str += "3,"; return nil }, 46 | func() error { str += "4,"; return nil }, 47 | ) 48 | 49 | tests := []struct { 50 | name string 51 | }{ 52 | { 53 | "testing", 54 | }, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | Clean() 59 | assert.Equal(t, str, "4,3,2,1,") 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // NewStack ... 8 | func NewStack() *DeferStack { 9 | return &DeferStack{ 10 | fns: make([]func() error, 0), 11 | mu: sync.RWMutex{}, 12 | } 13 | } 14 | 15 | // DeferStack ... 16 | type DeferStack struct { 17 | fns []func() error 18 | mu sync.RWMutex 19 | } 20 | 21 | // Push ... 22 | func (ds *DeferStack) Push(fns ...func() error) { 23 | ds.mu.Lock() 24 | defer ds.mu.Unlock() 25 | ds.fns = append(ds.fns, fns...) 26 | } 27 | 28 | // Clean ... 29 | func (ds *DeferStack) Clean() { 30 | ds.mu.Lock() 31 | defer ds.mu.Unlock() 32 | for i := len(ds.fns) - 1; i >= 0; i-- { 33 | _ = ds.fns[i]() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewStack(t *testing.T) { 8 | // stack := &DeferStack{ 9 | // fns: make([]func() error, 0), 10 | // mu: sync.RWMutex{}, 11 | // } 12 | stack := NewStack() 13 | state := "" 14 | fn1 := func() error { 15 | state = state + "1" 16 | return nil 17 | } 18 | fn2 := func() error { 19 | state = state + "2" 20 | return nil 21 | } 22 | fn3 := func() error { 23 | state = state + "3" 24 | return nil 25 | } 26 | stack.Push(fn1, fn2) 27 | stack.Push(fn3) 28 | stack.Clean() 29 | want := "321" 30 | if state != want { 31 | t.Fatalf("Stack has error,want:%v ret:%v", want, state) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/task/consistenthash/consistenthash.go: -------------------------------------------------------------------------------- 1 | package consistenthash 2 | 3 | /* 4 | Copyright 2013 Google Inc. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Package consistenthash provides an implementation of a ring hash. 20 | 21 | import ( 22 | "hash/crc32" 23 | "sort" 24 | "strconv" 25 | ) 26 | 27 | type Hash func(data []byte) uint32 28 | 29 | type Map struct { 30 | hash Hash 31 | replicas int 32 | keys []int // Sorted 33 | hashMap map[int]string 34 | } 35 | 36 | func New(replicas int, fn Hash) *Map { 37 | m := &Map{ 38 | replicas: replicas, 39 | hash: fn, 40 | hashMap: make(map[int]string), 41 | } 42 | if m.hash == nil { 43 | m.hash = crc32.ChecksumIEEE 44 | } 45 | return m 46 | } 47 | 48 | // Returns true if there are no items available. 49 | func (m *Map) IsEmpty() bool { 50 | return len(m.keys) == 0 51 | } 52 | 53 | // Adds some keys to the hash. 54 | func (m *Map) Add(keys ...string) { 55 | for _, key := range keys { 56 | for i := 0; i < m.replicas; i++ { 57 | // use replicas id + _ + key to avoid the key has pre-number. 58 | hash := int(m.hash([]byte(strconv.Itoa(i) + "_" + key))) 59 | if m.hashMap[hash] == "" { 60 | m.keys = append(m.keys, hash) 61 | m.hashMap[hash] = key 62 | } 63 | } 64 | } 65 | sort.Ints(m.keys) 66 | } 67 | 68 | // Gets the closest item in the hash to the provided key. 69 | func (m *Map) Get(key string) string { 70 | if m.IsEmpty() { 71 | return "" 72 | } 73 | hash := int(m.hash([]byte(key))) 74 | // Binary search for appropriate replica. 75 | idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash }) 76 | // Means we have cycled back to the first replica. 77 | if idx == len(m.keys) { 78 | idx = 0 79 | } 80 | return m.hashMap[m.keys[idx]] 81 | } 82 | -------------------------------------------------------------------------------- /core/task/cron/README.md: -------------------------------------------------------------------------------- 1 | # cron 2 | 3 | fork from [robfig/cron](github.com/robfig/cron) 4 | 5 | maintain by Task opensource team. -------------------------------------------------------------------------------- /core/task/cron/chain.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | // JobWrapper decorates the given Job with some behavior. 4 | type JobWrapper func(Job) Job 5 | 6 | // Chain is a sequence of JobWrappers that decorates submitted jobs with 7 | // cross-cutting behaviors like logging or synchronization. 8 | type Chain struct { 9 | wrappers []JobWrapper 10 | } 11 | 12 | // NewChain returns a Chain consisting of the given JobWrappers. 13 | func NewChain(c ...JobWrapper) Chain { 14 | return Chain{c} 15 | } 16 | 17 | // Then decorates the given job with all JobWrappers in the chain. 18 | // 19 | // This: 20 | // 21 | // NewChain(m1, m2, m3).Then(job) 22 | // 23 | // is equivalent to: 24 | // 25 | // m1(m2(m3(job))) 26 | func (c Chain) Then(j Job) Job { 27 | for i := range c.wrappers { 28 | j = c.wrappers[len(c.wrappers)-i-1](j) 29 | } 30 | return j 31 | } 32 | -------------------------------------------------------------------------------- /core/task/cron/constantdelay.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import "time" 4 | 5 | // ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". 6 | // It does not support jobs more frequent than once a second. 7 | type ConstantDelaySchedule struct { 8 | Delay time.Duration 9 | } 10 | 11 | // Every returns a crontab Schedule that activates once every duration. 12 | // Delays of less than a second are not supported (will round up to 1 second). 13 | // Any fields less than a Second are truncated. 14 | func Every(duration time.Duration) ConstantDelaySchedule { 15 | if duration < time.Second { 16 | duration = time.Second 17 | } 18 | return ConstantDelaySchedule{ 19 | Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, 20 | } 21 | } 22 | 23 | // Next returns the next time this should be run. 24 | // This rounds so that the next activation time will be on the second. 25 | func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { 26 | return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) 27 | } 28 | -------------------------------------------------------------------------------- /core/task/cron/option.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Option represents a modification to the default behavior of a Cron. 8 | type Option func(*Cron) 9 | 10 | // WithLocation overrides the timezone of the cron instance. 11 | func WithLocation(loc *time.Location) Option { 12 | return func(c *Cron) { 13 | c.location = loc 14 | } 15 | } 16 | 17 | // WithSeconds overrides the parser used for interpreting job schedules to 18 | // include a seconds field as the first one. 19 | func WithSeconds() Option { 20 | return WithParser(NewParser( 21 | Second | Minute | Hour | Dom | Month | Dow | Descriptor, 22 | )) 23 | } 24 | 25 | // WithParser overrides the parser used for interpreting job schedules. 26 | func WithParser(p ScheduleParser) Option { 27 | return func(c *Cron) { 28 | c.parser = p 29 | } 30 | } 31 | 32 | // WithChain specifies Job wrappers to apply to all jobs added to this cron. 33 | // Refer to the Chain* functions in this package for provided wrappers. 34 | func WithChain(wrappers ...JobWrapper) Option { 35 | return func(c *Cron) { 36 | c.chain = NewChain(wrappers...) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/task/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/client/etcdv3" 7 | "github.com/abulo/ratel/v3/stores/redis" 8 | ) 9 | 10 | // There is only one driver for one task. 11 | // Tips for write a user-defined Driver by yourself. 12 | // 1. Confirm that `Stop` and `Start` can be called for more times. 13 | // 2. Must make `GetNodes` will return error when timeout. 14 | type DriverV2 interface { 15 | // init driver 16 | Init(serviceName string, opts ...Option) 17 | // get nodeID 18 | NodeID() string 19 | // get nodes 20 | GetNodes(ctx context.Context) (nodes []string, err error) 21 | 22 | // register node to remote server (like etcd/redis), 23 | // will create a goroutine to keep the connection. 24 | // And then continue for other work. 25 | Start(ctx context.Context) (err error) 26 | 27 | // stop the goroutine of keep connection. 28 | Stop(ctx context.Context) (err error) 29 | 30 | WithOption(opt Option) (err error) 31 | } 32 | 33 | func NewRedisDriver(redisClient *redis.Client) DriverV2 { 34 | return newRedisDriver(redisClient) 35 | } 36 | 37 | func NewEtcdDriver(etcdCli *etcdv3.Client) DriverV2 { 38 | return newEtcdDriver(etcdCli) 39 | } 40 | 41 | func NewRedisZSetDriver(redisClient *redis.Client) DriverV2 { 42 | return newRedisZSetDriver(redisClient) 43 | } 44 | -------------------------------------------------------------------------------- /core/task/driver/option.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | OptionTypeTimeout = 0x600 9 | ) 10 | 11 | type Option interface { 12 | Type() int 13 | } 14 | 15 | type TimeoutOption struct{ timeout time.Duration } 16 | 17 | func (to TimeoutOption) Type() int { return OptionTypeTimeout } 18 | func NewTimeoutOption(timeout time.Duration) TimeoutOption { return TimeoutOption{timeout: timeout} } 19 | -------------------------------------------------------------------------------- /core/task/driver/util.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // GlobalKeyPrefix is a global redis key prefix 10 | const GlobalKeyPrefix = "Distributed-Cron:" 11 | 12 | func GetKeyPre(serviceName string) string { 13 | return GlobalKeyPrefix + serviceName + ":" 14 | } 15 | 16 | func GetNodeId(serviceName string) string { 17 | return GetKeyPre(serviceName) + uuid.New().String() 18 | } 19 | 20 | func TimePre(t time.Time, preDuration time.Duration) int64 { 21 | return t.Add(-preDuration).Unix() 22 | } 23 | -------------------------------------------------------------------------------- /core/task/inodepool.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | ErrNodePoolIsUpgrading = errors.New("nodePool is upgrading") 11 | ErrNodePoolIsNil = errors.New("nodePool is nil") 12 | ) 13 | 14 | type INodePool interface { 15 | Start(ctx context.Context) error 16 | CheckJobAvailable(jobName string) (bool, error) 17 | Stop(ctx context.Context) error 18 | 19 | GetNodeID() string 20 | GetLastNodesUpdateTime() time.Time 21 | } 22 | -------------------------------------------------------------------------------- /core/task/irecentjobpacker.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "time" 4 | 5 | // IRecentJobPacker 6 | // this is an interface which be used to 7 | // pack the jobs running in the cluster state 8 | // is `unstable`. 9 | // like some nodes broken or new nodes were add. 10 | type IRecentJobPacker interface { 11 | // goroutine safety. 12 | // Add a job to packer 13 | // will save recent jobs (like 2 * heartbeat duration) 14 | AddJob(jobName string, t time.Time) error 15 | 16 | // goroutine safety. 17 | // Pop out all jobs in packer 18 | PopAllJobs() (jobNames []string) 19 | } 20 | -------------------------------------------------------------------------------- /core/task/job_warpper.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "github.com/abulo/ratel/v3/core/task/cron" 4 | 5 | // Job Interface 6 | type Job interface { 7 | Run() 8 | } 9 | 10 | // This type of Job will be 11 | // recovered in a node of service 12 | // restarting. 13 | type StableJob interface { 14 | Job 15 | GetCron() string 16 | Serialize() ([]byte, error) 17 | UnSerialize([]byte) error 18 | } 19 | 20 | // JobWarpper is a job warpper 21 | type JobWarpper struct { 22 | ID cron.EntryID 23 | Task *Task 24 | Name string 25 | CronStr string 26 | Job Job 27 | } 28 | 29 | // Run is run job 30 | func (job JobWarpper) Run() { 31 | //如果该任务分配给了这个节点 则允许执行 32 | if job.Task.allowThisNodeRun(job.Name) { 33 | job.Execute() 34 | } 35 | } 36 | 37 | func (job JobWarpper) Execute() { 38 | if job.Job != nil { 39 | job.Job.Run() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/task/option.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/abulo/ratel/v3/core/task/cron" 7 | ) 8 | 9 | // Option is Task Option 10 | type Option func(*Task) 11 | 12 | // WithNodeUpdateDuration set node update duration 13 | func WithNodeUpdateDuration(d time.Duration) Option { 14 | return func(task *Task) { 15 | task.nodeUpdateDuration = d 16 | } 17 | } 18 | 19 | // WithHashReplicas set hashReplicas 20 | func WithHashReplicas(d int) Option { 21 | return func(task *Task) { 22 | task.hashReplicas = d 23 | } 24 | } 25 | 26 | // CronOptionLocation is warp cron with location 27 | func CronOptionLocation(loc *time.Location) Option { 28 | return func(task *Task) { 29 | f := cron.WithLocation(loc) 30 | task.crOptions = append(task.crOptions, f) 31 | } 32 | } 33 | 34 | // CronOptionSeconds is warp cron with seconds 35 | func CronOptionSeconds() Option { 36 | return func(task *Task) { 37 | f := cron.WithSeconds() 38 | task.crOptions = append(task.crOptions, f) 39 | } 40 | } 41 | 42 | // CronOptionParser is warp cron with schedules. 43 | func CronOptionParser(p cron.ScheduleParser) Option { 44 | return func(task *Task) { 45 | f := cron.WithParser(p) 46 | task.crOptions = append(task.crOptions, f) 47 | } 48 | } 49 | 50 | // CronOptionChain is Warp cron with chain 51 | func CronOptionChain(wrappers ...cron.JobWrapper) Option { 52 | return func(task *Task) { 53 | f := cron.WithChain(wrappers...) 54 | task.crOptions = append(task.crOptions, f) 55 | } 56 | } 57 | 58 | // You can defined yourself recover function to make the 59 | // job will be added to your task when the process restart 60 | func WithRecoverFunc(recoverFunc RecoverFuncType) Option { 61 | return func(task *Task) { 62 | task.RecoverFunc = recoverFunc 63 | } 64 | } 65 | 66 | // You can use this option to start the recent jobs rerun 67 | // after the cluster upgrading. 68 | func WithClusterStable(timeWindow time.Duration) Option { 69 | return func(d *Task) { 70 | d.recentJobs = NewRecentJobPacker(timeWindow) 71 | } 72 | } 73 | 74 | func RunningLocally() Option { 75 | return func(d *Task) { 76 | d.runningLocally = true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /core/task/recentjobpacker.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "container/heap" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type JobWithTime struct { 10 | JobName string 11 | RunningTime time.Time 12 | } 13 | 14 | type JobWithTimeHeap []JobWithTime 15 | 16 | func (jobHeap *JobWithTimeHeap) Pop() (ret any) { 17 | n := jobHeap.Len() - 1 18 | ret = (*jobHeap)[n] 19 | (*jobHeap) = (*jobHeap)[0:n] 20 | return 21 | } 22 | 23 | func (jobHeap *JobWithTimeHeap) Push(x any) { 24 | (*jobHeap) = append((*jobHeap), x.(JobWithTime)) 25 | } 26 | 27 | func (jobHeap *JobWithTimeHeap) Less(i, j int) bool { 28 | return (*jobHeap)[i].RunningTime.Before((*jobHeap)[j].RunningTime) 29 | } 30 | 31 | func (jobHeap *JobWithTimeHeap) Len() int { 32 | return len(*jobHeap) 33 | } 34 | 35 | func (jobHeap *JobWithTimeHeap) Swap(i, j int) { 36 | t := (*jobHeap)[i] 37 | (*jobHeap)[i] = (*jobHeap)[j] 38 | (*jobHeap)[j] = t 39 | } 40 | 41 | func (jobHeap *JobWithTimeHeap) Index(i int) any { 42 | return (*jobHeap)[i] 43 | } 44 | 45 | type RecentJobPacker struct { 46 | sync.Mutex 47 | timeWindow time.Duration 48 | recentJobs JobWithTimeHeap 49 | } 50 | 51 | func (rjp *RecentJobPacker) AddJob(jobName string, t time.Time) (err error) { 52 | rjp.Lock() 53 | defer rjp.Unlock() 54 | heap.Push(&rjp.recentJobs, JobWithTime{ 55 | JobName: jobName, 56 | RunningTime: t, 57 | }) 58 | 59 | unRencentTime := time.Now().Add(-rjp.timeWindow) 60 | for rjp.recentJobs.Len() > 0 && rjp.recentJobs.Index(0).(JobWithTime).RunningTime.Before(unRencentTime) { 61 | heap.Pop(&rjp.recentJobs) 62 | } 63 | return 64 | } 65 | 66 | func (rjp *RecentJobPacker) PopAllJobs() (jobNames []string) { 67 | rjp.Lock() 68 | defer rjp.Unlock() 69 | jobNames = make([]string, 0) 70 | for rjp.recentJobs.Len() > 0 { 71 | jobNames = append(jobNames, heap.Pop(&rjp.recentJobs).(JobWithTime).JobName) 72 | } 73 | rjp.recentJobs = make(JobWithTimeHeap, 0) 74 | heap.Init(&rjp.recentJobs) 75 | return 76 | } 77 | 78 | func NewRecentJobPacker(timeWindow time.Duration) IRecentJobPacker { 79 | return &RecentJobPacker{ 80 | timeWindow: timeWindow, 81 | recentJobs: make([]JobWithTime, 0), 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/trace/carrier.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "go.opentelemetry.io/otel/propagation" 5 | "google.golang.org/grpc/metadata" 6 | ) 7 | 8 | // assert that MetadataReaderWriter implements the TextMapCarrier interface 9 | var _ propagation.TextMapCarrier = (*MetadataReaderWriter)(nil) 10 | 11 | // MetadataReaderWriter ... 12 | type MetadataReaderWriter metadata.MD 13 | 14 | func (m MetadataReaderWriter) Get(key string) string { 15 | values := metadata.MD(m).Get(key) 16 | if len(values) == 0 { 17 | return "" 18 | } 19 | return values[0] 20 | } 21 | 22 | func (m MetadataReaderWriter) Set(key, value string) { 23 | metadata.MD(m).Set(key, value) 24 | } 25 | 26 | func (m MetadataReaderWriter) Keys() []string { 27 | keys := make([]string, 0, len(m)) 28 | for k := range metadata.MD(m) { 29 | keys = append(keys, k) 30 | } 31 | return keys 32 | } 33 | -------------------------------------------------------------------------------- /core/trace/config.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/core/logger" 7 | "go.opentelemetry.io/otel" 8 | "go.opentelemetry.io/otel/exporters/jaeger" 9 | "go.opentelemetry.io/otel/sdk/resource" 10 | sdk "go.opentelemetry.io/otel/sdk/trace" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.24.0" 12 | "go.opentelemetry.io/otel/trace" 13 | ) 14 | 15 | type Config struct { 16 | Name string 17 | Endpoint string 18 | Sampler float64 19 | } 20 | 21 | // Option 选项 22 | type Option func(r *Config) 23 | 24 | // WithName 设置名称 25 | func WithName(name string) Option { 26 | return func(r *Config) { 27 | r.Name = name 28 | } 29 | } 30 | 31 | // WithEndpoint 设置端点 32 | func WithEndpoint(endpoint string) Option { 33 | return func(r *Config) { 34 | r.Endpoint = endpoint 35 | } 36 | } 37 | 38 | // WithSampler 设置采样率 39 | func WithSampler(sampler float64) Option { 40 | return func(r *Config) { 41 | r.Sampler = sampler 42 | } 43 | } 44 | 45 | func NewConfig(opts ...Option) *Config { 46 | c := &Config{} 47 | for _, opt := range opts { 48 | opt(c) 49 | } 50 | return c 51 | } 52 | 53 | func (config *Config) Build() trace.TracerProvider { 54 | exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(config.Endpoint))) 55 | if err != nil { 56 | logger.Logger.Panic("new jaeger", err) 57 | return nil 58 | } 59 | 60 | resource, err := resource.New(context.TODO(), 61 | resource.WithHost(), 62 | resource.WithFromEnv(), 63 | resource.WithTelemetrySDK(), 64 | resource.WithAttributes( 65 | semconv.ServiceNameKey.String(config.Name), 66 | ), 67 | ) 68 | if err != nil { 69 | logger.Logger.Panic("new resource", err) 70 | return nil 71 | } 72 | tp := sdk.NewTracerProvider( 73 | sdk.WithSampler(sdk.ParentBased(sdk.TraceIDRatioBased(config.Sampler))), 74 | sdk.WithBatcher(exp), 75 | sdk.WithResource(resource), 76 | ) 77 | otel.SetTracerProvider(tp) 78 | return tp 79 | } 80 | -------------------------------------------------------------------------------- /core/trace/trace.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/core/logger" 7 | "github.com/opentracing/opentracing-go" 8 | "go.opentelemetry.io/otel" 9 | otelOpentracing "go.opentelemetry.io/otel/bridge/opentracing" 10 | "go.opentelemetry.io/otel/propagation" 11 | "go.opentelemetry.io/otel/trace" 12 | ) 13 | 14 | // SetGlobalTracer ... 15 | func SetGlobalTracer(tp trace.TracerProvider) { 16 | logger.Logger.Info("set global tracer") 17 | 18 | propagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, Jaeger{}) 19 | 20 | // be compatible with opentracing 21 | bridge, wrapperTracerProvider := otelOpentracing.NewTracerPair(tp.Tracer("")) 22 | bridge.SetTextMapPropagator(propagator) 23 | opentracing.SetGlobalTracer(bridge) 24 | 25 | otel.SetTextMapPropagator(propagator) 26 | otel.SetTracerProvider(wrapperTracerProvider) 27 | } 28 | 29 | // Tracer is otel span tracer 30 | type Tracer struct { 31 | tracer trace.Tracer 32 | kind trace.SpanKind 33 | } 34 | 35 | // NewTracer create tracer instance 36 | func NewTracer(kind trace.SpanKind) *Tracer { 37 | return &Tracer{tracer: otel.Tracer("ratel"), kind: kind} 38 | } 39 | 40 | // Start start tracing span 41 | func (t *Tracer) Start(ctx context.Context, operation string, carrier propagation.TextMapCarrier, opts ...trace.SpanStartOption) (context.Context, trace.Span) { 42 | if (t.kind == trace.SpanKindServer || t.kind == trace.SpanKindConsumer) && carrier != nil { 43 | ctx = otel.GetTextMapPropagator().Extract(ctx, carrier) 44 | } 45 | opts = append(opts, trace.WithSpanKind(t.kind)) 46 | 47 | ctx, span := t.tracer.Start(ctx, operation, opts...) 48 | 49 | if (t.kind == trace.SpanKindClient || t.kind == trace.SpanKindProducer) && carrier != nil { 50 | otel.GetTextMapPropagator().Inject(ctx, carrier) 51 | } 52 | return ctx, span 53 | } 54 | -------------------------------------------------------------------------------- /core/trace/util.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | semconv "go.opentelemetry.io/otel/semconv/v1.24.0" 9 | ) 10 | 11 | // ParseFullMethod returns the method name and attributes. 12 | func ParseFullMethod(fullMethod string) (string, []attribute.KeyValue) { 13 | name := strings.TrimLeft(fullMethod, "/") 14 | parts := strings.SplitN(name, "/", 2) 15 | if len(parts) != 2 { 16 | // Invalid format, does not follow `/package.service/method`. 17 | return name, []attribute.KeyValue(nil) 18 | } 19 | 20 | var attrs []attribute.KeyValue 21 | if service := parts[0]; service != "" { 22 | attrs = append(attrs, semconv.RPCServiceKey.String(service)) 23 | } 24 | if method := parts[1]; method != "" { 25 | attrs = append(attrs, semconv.RPCMethodKey.String(method)) 26 | } 27 | 28 | return name, attrs 29 | } 30 | 31 | // PeerAttr returns the peer attributes. 32 | func PeerAttr(addr string) []attribute.KeyValue { 33 | host, port, err := net.SplitHostPort(addr) 34 | if err != nil { 35 | return nil 36 | } 37 | 38 | if len(host) == 0 { 39 | host = "127.0.0.1" 40 | } 41 | 42 | return []attribute.KeyValue{ 43 | semconv.NetHostNameKey.String(host), 44 | semconv.NetPeerPortKey.String(port), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /correct/format.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | const ( 8 | cjk = `\p{Han}|\p{Hangul}|\p{Hanunoo}|\p{Katakana}|\p{Hiragana}|\p{Bopomofo}` 9 | spaceRe = `[ ]` 10 | ) 11 | 12 | var ( 13 | // Strategies all rules 14 | strategies []*strategery 15 | dashHansRe = regexp.MustCompile(`([` + cjk + `)】」》”’])([\-]+)([` + cjk + `(【「《“‘])`) 16 | leftQuoteRe = regexp.MustCompile(spaceRe + `([(【「《])`) 17 | rightQuoteRe = regexp.MustCompile(`([)】」》])` + spaceRe) 18 | ) 19 | 20 | // RegisterStrategery a new strategery 21 | func registerStrategery(one, other string, space, reverse bool) { 22 | strategies = append(strategies, newStrategery(one, other, space, reverse)) 23 | } 24 | 25 | func init() { 26 | // EnglishLetter 27 | registerStrategery(cjk, `[a-zA-Z]`, true, true) 28 | 29 | // Number 30 | registerStrategery(cjk, `[0-9]`, true, true) 31 | 32 | // SpecialSymbol 33 | registerStrategery(cjk, `[\|+*]`, true, true) 34 | registerStrategery(cjk, `[@]`, true, false) 35 | registerStrategery(cjk, `[\[\(‘“]`, true, false) 36 | registerStrategery(`[’”\]\)!%]`, cjk, true, false) 37 | registerStrategery(`[”\]\)!]`, `[a-zA-Z0-9]+`, true, false) 38 | 39 | // FullwidthPunctuation 40 | registerStrategery(`[\w`+cjk+`]`, `[,。!?:;)」》】”’]`, false, true) 41 | registerStrategery(`[‘“【「《(]`, `[\w`+cjk+`]`, false, true) 42 | } 43 | 44 | func spaceDashWithHans(in string) (out string) { 45 | // 自由 - 开放 46 | out = dashHansRe.ReplaceAllString(in, "$1 $2 $3") 47 | out = leftQuoteRe.ReplaceAllString(out, "$1") 48 | out = rightQuoteRe.ReplaceAllString(out, "$1") 49 | return out 50 | } 51 | 52 | // Option ... 53 | type Option interface { 54 | Format(text string) string 55 | } 56 | 57 | // Format auto format string to add spaces between Chinese and English words. 58 | func Format(in string, options ...Option) (out string) { 59 | out = in 60 | 61 | out = halfwidth(out) 62 | out = fullwidth(out) 63 | 64 | for _, s := range strategies { 65 | out = s.format(out) 66 | } 67 | 68 | out = spaceDashWithHans(out) 69 | 70 | for _, opt := range options { 71 | out = opt.Format(out) 72 | } 73 | 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /correct/format_test.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "testing" 4 | 5 | func Test_registerStrategery(t *testing.T) { 6 | type args struct { 7 | one string 8 | other string 9 | space bool 10 | reverse bool 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | }{ 16 | // TODO: Add test cases. 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | registerStrategery(tt.args.one, tt.args.other, tt.args.space, tt.args.reverse) 21 | }) 22 | } 23 | } 24 | 25 | func Test_spaceDashWithHans(t *testing.T) { 26 | type args struct { 27 | in string 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | wantOut string 33 | }{ 34 | // TODO: Add test cases. 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if gotOut := spaceDashWithHans(tt.args.in); gotOut != tt.wantOut { 39 | t.Errorf("spaceDashWithHans() = %v, want %v", gotOut, tt.wantOut) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestFormat(t *testing.T) { 46 | type args struct { 47 | in string 48 | options []Option 49 | } 50 | tests := []struct { 51 | name string 52 | args args 53 | wantOut string 54 | }{ 55 | // TODO: Add test cases. 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | if gotOut := Format(tt.args.in, tt.args.options...); gotOut != tt.wantOut { 60 | t.Errorf("Format() = %v, want %v", gotOut, tt.wantOut) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /correct/fullwidth.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var ( 8 | fullwidthMaps = map[string]string{ 9 | ",": ",", 10 | ".": "。", 11 | ";": ";", 12 | ":": ":", 13 | "!": "!", 14 | "?": "?", 15 | "~": "~", 16 | // "(": "(", 17 | // ")": ")", 18 | } 19 | 20 | spcialPunctuations = `[.:]` 21 | normalPunctuations = `[,\!\?~]` 22 | 23 | punctuationWithLeftCJKRe = regexp.MustCompile(normalPunctuations + `[` + cjk + `]+`) 24 | punctuationWithRightCJKRe = regexp.MustCompile(`[` + cjk + `]+` + normalPunctuations) 25 | punctuationWithSpeicalCJKRe = regexp.MustCompile(`[` + cjk + `]+` + spcialPunctuations + `[` + cjk + `]+`) 26 | punctuationWithSpeicalLastCJKRe = regexp.MustCompile(`[` + cjk + `]+` + spcialPunctuations + "$") 27 | punctuationsRe = regexp.MustCompile(`(` + spcialPunctuations + `|` + normalPunctuations + `)`) 28 | ) 29 | 30 | // fullwidth correct punctuations near the CJK chars 31 | func fullwidth(text string) (out string) { 32 | out = text 33 | 34 | out = punctuationWithLeftCJKRe.ReplaceAllStringFunc(out, fullwidthReplacePart) 35 | out = punctuationWithRightCJKRe.ReplaceAllStringFunc(out, fullwidthReplacePart) 36 | out = punctuationWithSpeicalCJKRe.ReplaceAllStringFunc(out, fullwidthReplacePart) 37 | out = punctuationWithSpeicalLastCJKRe.ReplaceAllStringFunc(out, fullwidthReplacePart) 38 | 39 | return 40 | } 41 | 42 | func fullwidthReplacePart(part string) string { 43 | part = punctuationsRe.ReplaceAllStringFunc(part, func(str string) string { 44 | str = fullwidthMaps[str] 45 | return str 46 | }) 47 | 48 | return part 49 | 50 | } 51 | -------------------------------------------------------------------------------- /correct/fullwidth_test.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "testing" 4 | 5 | func Test_fullwidth(t *testing.T) { 6 | type args struct { 7 | text string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | wantOut string 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if gotOut := fullwidth(tt.args.text); gotOut != tt.wantOut { 19 | t.Errorf("fullwidth() = %v, want %v", gotOut, tt.wantOut) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func Test_fullwidthReplacePart(t *testing.T) { 26 | type args struct { 27 | part string 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want string 33 | }{ 34 | // TODO: Add test cases. 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := fullwidthReplacePart(tt.args.part); got != tt.want { 39 | t.Errorf("fullwidthReplacePart() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /correct/halfwidth.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | charWidthMap = map[rune]rune{ 10 | 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '0': '0', ' ': ' ', 11 | } 12 | 13 | halfTimeRe = regexp.MustCompile(`(\d)(:)(\d)`) 14 | ) 15 | 16 | func halfwidth(text string) string { 17 | runes := []rune{} 18 | for _, char := range text { 19 | newChar := charWidthMap[char] 20 | if newChar != 0 { 21 | runes = append(runes, charWidthMap[char]) 22 | } else { 23 | runes = append(runes, char) 24 | } 25 | } 26 | 27 | out := string(runes) 28 | 29 | // Fix 12:00 -> 12:00 30 | out = halfTimeRe.ReplaceAllStringFunc(out, func(part string) string { 31 | return strings.Replace(part, ":", ":", 1) 32 | }) 33 | 34 | return out 35 | } 36 | -------------------------------------------------------------------------------- /correct/halfwidth_test.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "testing" 4 | 5 | func Test_halfwidth(t *testing.T) { 6 | type args struct { 7 | text string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := halfwidth(tt.args.text); got != tt.want { 19 | t.Errorf("halfwidth() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /correct/html.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "regexp" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/tdewolff/parse/v2" 10 | "github.com/tdewolff/parse/v2/html" 11 | ) 12 | 13 | var ( 14 | ignoreTagsRe = regexp.MustCompile("(?mi)<(pre|script|style|textarea)") 15 | ) 16 | 17 | // FormatHTML format HTML content 18 | func FormatHTML(body string, options ...Option) (out string, err error) { 19 | return processHTML(body, func(text string) string { 20 | return Format(text, options...) 21 | }) 22 | } 23 | 24 | // UnformatHTML cleanup spaces for HTML 25 | func UnformatHTML(body string, options ...UnformatOption) (out string, err error) { 26 | return processHTML(body, func(text string) string { 27 | return Unformat(text, options...) 28 | }) 29 | } 30 | 31 | func processHTML(body string, fn func(plainText string) string) (out string, err error) { 32 | w := &bytes.Buffer{} 33 | lex := html.NewLexer(parse.NewInputString(body)) 34 | // defer lex.Restore() 35 | out = body 36 | 37 | ignoreTag := false 38 | 39 | for { 40 | t, data := lex.Next() 41 | 42 | switch t { 43 | case html.ErrorToken: 44 | if lex.Err() == io.EOF { 45 | return w.String(), nil 46 | } 47 | 48 | err = errors.Errorf("Error on line %v", lex.Err()) 49 | return 50 | case html.TextToken: 51 | if ignoreTag { 52 | if _, err := w.Write(data); err != nil { 53 | return out, err 54 | } 55 | 56 | ignoreTag = false 57 | continue 58 | } 59 | 60 | formated := fn(string(data)) 61 | if _, err := w.Write([]byte(formated)); err != nil { 62 | return out, err 63 | } 64 | case html.StartTagToken: 65 | if ignoreTagsRe.Match(data) { 66 | ignoreTag = true 67 | } 68 | 69 | if _, err := w.Write(data); err != nil { 70 | return out, err 71 | } 72 | 73 | default: 74 | if _, err := w.Write(data); err != nil { 75 | return out, err 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /correct/html_test.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "testing" 4 | 5 | func TestFormatHTML(t *testing.T) { 6 | type args struct { 7 | body string 8 | options []Option 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | wantOut string 14 | wantErr bool 15 | }{ 16 | // TODO: Add test cases. 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | gotOut, err := FormatHTML(tt.args.body, tt.args.options...) 21 | if (err != nil) != tt.wantErr { 22 | t.Errorf("FormatHTML() error = %v, wantErr %v", err, tt.wantErr) 23 | return 24 | } 25 | if gotOut != tt.wantOut { 26 | t.Errorf("FormatHTML() = %v, want %v", gotOut, tt.wantOut) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func TestUnformatHTML(t *testing.T) { 33 | type args struct { 34 | body string 35 | options []UnformatOption 36 | } 37 | tests := []struct { 38 | name string 39 | args args 40 | wantOut string 41 | wantErr bool 42 | }{ 43 | // TODO: Add test cases. 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | gotOut, err := UnformatHTML(tt.args.body, tt.args.options...) 48 | if (err != nil) != tt.wantErr { 49 | t.Errorf("UnformatHTML() error = %v, wantErr %v", err, tt.wantErr) 50 | return 51 | } 52 | if gotOut != tt.wantOut { 53 | t.Errorf("UnformatHTML() = %v, want %v", gotOut, tt.wantOut) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func Test_processHTML(t *testing.T) { 60 | type args struct { 61 | body string 62 | fn func(plainText string) string 63 | } 64 | tests := []struct { 65 | name string 66 | args args 67 | wantOut string 68 | wantErr bool 69 | }{ 70 | // TODO: Add test cases. 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | gotOut, err := processHTML(tt.args.body, tt.args.fn) 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("processHTML() error = %v, wantErr %v", err, tt.wantErr) 77 | return 78 | } 79 | if gotOut != tt.wantOut { 80 | t.Errorf("processHTML() = %v, want %v", gotOut, tt.wantOut) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /correct/strategery.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "regexp" 4 | 5 | // Strategery for define rule 6 | type strategery struct { 7 | addSpaceRe *regexp.Regexp 8 | addSpaceReverseRe *regexp.Regexp 9 | removeSpaceRe *regexp.Regexp 10 | removeSpaceReverseRe *regexp.Regexp 11 | 12 | space bool 13 | reverse bool 14 | } 15 | 16 | func newStrategery(one, other string, space, reverse bool) *strategery { 17 | addSpaceStr := "(" + one + `)(` + other + ")" 18 | addSpaceReverseStr := "(" + other + `)(` + one + ")" 19 | 20 | removeSpaceStr := `(` + one + `)` + spaceRe + `(` + other + `)` 21 | removeSpaceReverseStr := "(" + other + `)` + spaceRe + `(` + one + ")" 22 | 23 | return &strategery{ 24 | addSpaceRe: regexp.MustCompile(addSpaceStr), 25 | addSpaceReverseRe: regexp.MustCompile(addSpaceReverseStr), 26 | removeSpaceRe: regexp.MustCompile(removeSpaceStr), 27 | removeSpaceReverseRe: regexp.MustCompile(removeSpaceReverseStr), 28 | space: space, 29 | reverse: reverse, 30 | } 31 | } 32 | 33 | func (s *strategery) format(in string) (out string) { 34 | out = in 35 | if s.space { 36 | out = s.addSpace(out) 37 | } else { 38 | out = s.removeSpace(out) 39 | } 40 | 41 | return 42 | } 43 | 44 | func (s *strategery) addSpace(in string) (out string) { 45 | out = in 46 | out = s.addSpaceRe.ReplaceAllString(out, "$1 $2") 47 | if s.reverse { 48 | out = s.addSpaceReverseRe.ReplaceAllString(out, "$1 $2") 49 | } 50 | 51 | return 52 | } 53 | 54 | func (s *strategery) removeSpace(in string) (out string) { 55 | out = in 56 | out = s.removeSpaceRe.ReplaceAllString(out, "$1 $2") 57 | if s.reverse { 58 | out = s.removeSpaceReverseRe.ReplaceAllString(out, "$1 $2") 59 | } 60 | 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /correct/strategery_test.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_newStrategery(t *testing.T) { 9 | type args struct { 10 | one string 11 | other string 12 | space bool 13 | reverse bool 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want *strategery 19 | }{ 20 | // TODO: Add test cases. 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | if got := newStrategery(tt.args.one, tt.args.other, tt.args.space, tt.args.reverse); !reflect.DeepEqual(got, tt.want) { 25 | t.Errorf("newStrategery() = %v, want %v", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func Test_strategery_format(t *testing.T) { 32 | type args struct { 33 | in string 34 | } 35 | tests := []struct { 36 | name string 37 | s *strategery 38 | args args 39 | wantOut string 40 | }{ 41 | // TODO: Add test cases. 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | if gotOut := tt.s.format(tt.args.in); gotOut != tt.wantOut { 46 | t.Errorf("strategery.format() = %v, want %v", gotOut, tt.wantOut) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func Test_strategery_addSpace(t *testing.T) { 53 | type args struct { 54 | in string 55 | } 56 | tests := []struct { 57 | name string 58 | s *strategery 59 | args args 60 | wantOut string 61 | }{ 62 | // TODO: Add test cases. 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | if gotOut := tt.s.addSpace(tt.args.in); gotOut != tt.wantOut { 67 | t.Errorf("strategery.addSpace() = %v, want %v", gotOut, tt.wantOut) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func Test_strategery_removeSpace(t *testing.T) { 74 | type args struct { 75 | in string 76 | } 77 | tests := []struct { 78 | name string 79 | s *strategery 80 | args args 81 | wantOut string 82 | }{ 83 | // TODO: Add test cases. 84 | } 85 | for _, tt := range tests { 86 | t.Run(tt.name, func(t *testing.T) { 87 | if gotOut := tt.s.removeSpace(tt.args.in); gotOut != tt.wantOut { 88 | t.Errorf("strategery.removeSpace() = %v, want %v", gotOut, tt.wantOut) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /correct/unformat.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "regexp" 4 | 5 | var ( 6 | removeSpaceRe = regexp.MustCompile(`(` + spaceRe + `+)?(` + cjk + `)(` + spaceRe + `+)?`) 7 | ) 8 | 9 | // UnformatOption addition unformat options 10 | type UnformatOption interface { 11 | Unformat(text string) string 12 | } 13 | 14 | // Unformat to remove all spaces 15 | func Unformat(text string, options ...UnformatOption) string { 16 | text = removeSpaceRe.ReplaceAllString(text, "$2") 17 | 18 | for _, opt := range options { 19 | text = opt.Unformat(text) 20 | } 21 | 22 | return text 23 | } 24 | -------------------------------------------------------------------------------- /correct/unformat_test.go: -------------------------------------------------------------------------------- 1 | package correct 2 | 3 | import "testing" 4 | 5 | func TestUnformat(t *testing.T) { 6 | type args struct { 7 | text string 8 | options []UnformatOption 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | want string 14 | }{ 15 | // TODO: Add test cases. 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | if got := Unformat(tt.args.text, tt.args.options...); got != tt.want { 20 | t.Errorf("Unformat() = %v, want %v", got, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /coverage.txt: -------------------------------------------------------------------------------- 1 | mode: atomic 2 | -------------------------------------------------------------------------------- /filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | type ResultWriter struct { 8 | count int 9 | } 10 | 11 | func (_this *ResultWriter) Write(_ *Result) (stop bool) { 12 | _this.count++ 13 | return true 14 | } 15 | 16 | func (_this *ResultWriter) Len() int { 17 | return _this.count 18 | } 19 | 20 | func skipStr(skip ...string) []rune { 21 | if len(skip) > 0 { 22 | runes := []rune(skip[0]) 23 | sort.Slice(runes, func(i, j int) bool { 24 | return runes[i] < runes[j] 25 | }) 26 | return runes 27 | } 28 | return []rune(SortedSkipList()) 29 | } 30 | 31 | // Strings 将输入的敏感词列表转换成 tireRoot 树 32 | func Strings(words []string, skip ...string) *Search { 33 | search := NewSearch(SetSortedRunesSkip(skipStr(skip...))) 34 | search.TrieWriter().InsertWords(words).BuildFail() 35 | return search 36 | } 37 | 38 | func File(filename string, skip ...string) (search *Search, err error) { 39 | search = NewSearch(SetSortedRunesSkip(skipStr(skip...))) 40 | writer := search.TrieWriter() 41 | writer.InsertFile(filename) 42 | writer.BuildFail() 43 | return &Search{writer}, nil 44 | } 45 | 46 | type options struct { 47 | writer *TrieWriter 48 | skip *Skip 49 | } 50 | 51 | type Option func(options *options) 52 | 53 | func SetWriter(w *TrieWriter) Option { 54 | return func(options *options) { 55 | options.writer = w 56 | } 57 | } 58 | 59 | func SetSortedRunesSkip(s []rune) Option { 60 | return func(options *options) { 61 | skip := &Skip{list: s} 62 | options.skip = skip 63 | } 64 | } 65 | 66 | func SetSortedSkip(s string) Option { 67 | return func(options *options) { 68 | skip := &Skip{} 69 | skip.SetSorted(s) 70 | options.skip = skip 71 | } 72 | } 73 | 74 | func SetSkip(s string) Option { 75 | return func(options *options) { 76 | skip := &Skip{} 77 | skip.Set(s) 78 | options.skip = skip 79 | } 80 | } 81 | 82 | func NewSearch(opts ...Option) *Search { 83 | opt := &options{ 84 | skip: &Skip{list: []rune(sortedSkipList)}, 85 | writer: NewTrieWriter(), 86 | } 87 | for _, o := range opts { 88 | o(opt) 89 | } 90 | opt.writer.setSkip(opt.skip) 91 | return &Search{opt.writer} 92 | } 93 | -------------------------------------------------------------------------------- /filter/readme.md: -------------------------------------------------------------------------------- 1 | words := []string{"林茹", "林如", "临蓐", "空子", "霸王龙", "我是个SB", "是我", "TMD", "他妈的", "他妈"} 2 | handel := filter.Strings(words) 3 | str := []byte("我空ss子sss我是霸**王*龙,我是我我是个(S)(B)真的,TMD,他妈的") 4 | fmt.Println(handel.Find(str)) 5 | fmt.Println(string(handel.Replace(str, '*'))) 6 | fmt.Println(string(handel.ReplaceRune(str, '*'))) -------------------------------------------------------------------------------- /filter/result.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Result struct { 8 | Word string `json:"word"` // 匹配到的敏感词 9 | Matched string `json:"matched"` // 匹配到的字符串 10 | Start int `json:"start"` // 原始字符串中匹配到的起始位置 11 | End int `json:"end"` // 原始字符串中匹配到的结束位置 12 | } 13 | 14 | func (_this *Result) String() string { 15 | return fmt.Sprintf("word:%s mathced:%s start:%d end:%d;", _this.Word, _this.Matched, _this.Start, _this.End) 16 | } 17 | -------------------------------------------------------------------------------- /filter/skip.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | const sortedSkipList = "\n\r!\"#$%&'()*+-:;=@[]^_{|}~¤§¨°±·×÷ˉΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψω—―‖‘’“”…‰※€℃№ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ←↑→↓∈∏∑√∝∞∠∥∧∨∩∪∫∮∴∵∶∷∽≈≌≠≡≤≥≮≯⊙⊥⌒①②③④⑤⑥⑦⑧⑨⑩⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛─━│┃┄┅┆┇┈┉┊┋┌┍┎┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋■□▲△◆◇○◎●★☆♀♂、、、。。〃々〈〉《《》》「」『』【【】】〓〔〕〖〗㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩︿!"#&'()+,,-./:;<=>??@[\]_`{|}~ ̄" 8 | 9 | func SortedSkipList() string { 10 | return sortedSkipList 11 | } 12 | 13 | type Skip struct { 14 | list []rune 15 | } 16 | 17 | func (_this *Skip) Set(s string) { 18 | list := []rune(s) 19 | sort.Slice(list, func(i, j int) bool { 20 | return list[i] < list[j] 21 | }) 22 | _this.list = list 23 | } 24 | 25 | func (_this *Skip) SetSorted(s string) { 26 | list := []rune(s) 27 | _this.list = list 28 | } 29 | 30 | func (_this *Skip) ShouldSkip(r rune) bool { 31 | left, right := 0, len(_this.list) 32 | if right == 0 { 33 | return false 34 | } 35 | if r < _this.list[0] || r > _this.list[right-1] { 36 | return false 37 | } 38 | for left < right { 39 | mid := (left + right) >> 1 40 | if _this.list[mid] == r { 41 | return true 42 | } else if _this.list[mid] > r { 43 | right = mid 44 | } else { 45 | left = mid + 1 46 | } 47 | } 48 | return false 49 | } 50 | 51 | func (_this *Skip) String() string { 52 | return string(_this.list) 53 | } 54 | -------------------------------------------------------------------------------- /fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 程序所在目录 3 | path=$(cd `dirname $0`; pwd) 4 | for file in `ls $path`; do 5 | if [ -d $path"/"$file ];then 6 | if [ "vendor" != $file ];then 7 | goimports -w $path"/"$file 8 | gofmt -w $path"/"$file 9 | fi 10 | fi 11 | done -------------------------------------------------------------------------------- /img/fontx/font.go: -------------------------------------------------------------------------------- 1 | package fontx 2 | 3 | import ( 4 | "image/color" 5 | "os" 6 | 7 | "github.com/golang/freetype" 8 | "github.com/golang/freetype/truetype" 9 | "golang.org/x/image/font" 10 | ) 11 | 12 | // FontConfig ... 13 | type FontConfig struct { 14 | Font *truetype.Font 15 | FontSize float64 16 | Color color.Color 17 | } 18 | 19 | // TrueTypeFont ... 20 | type TrueTypeFont struct { 21 | Font *truetype.Font 22 | } 23 | 24 | // LoadFont loads a font file and returns *truetype.Font. 25 | func LoadFont(fontFile string) (*TrueTypeFont, error) { 26 | // Read the font data. 27 | fontBytes, err := os.ReadFile(fontFile) 28 | if err != nil { 29 | return nil, err 30 | } 31 | f, err := freetype.ParseFont(fontBytes) 32 | return &TrueTypeFont{f}, err 33 | } 34 | 35 | // NewFreeTypeContext ... 36 | func NewFreeTypeContext() *freetype.Context { 37 | return freetype.NewContext() 38 | } 39 | 40 | // GetMetrics gets font metrics by options. 41 | func (fo *TrueTypeFont) GetMetrics(fc *FontConfig) font.Metrics { 42 | opt := &truetype.Options{ 43 | Size: fc.FontSize, 44 | DPI: 72, 45 | Hinting: font.HintingNone, 46 | } 47 | face := truetype.NewFace(fo.Font, opt) 48 | return face.Metrics() 49 | } 50 | -------------------------------------------------------------------------------- /lang/i18n/readme.md: -------------------------------------------------------------------------------- 1 | ## 功能简介 2 | 3 | - 使用简单,支持加载多个语言,多个文件 4 | - 两种数据加载模式:单文件 `FileMode` 、文件夹 `DirMode`;默认是文件夹模式 5 | - 支持设置默认语言,备用语言;当在默认语言数据没找到时,自动尝试到备用语言查找 6 | - 支持参数替换,也有两种模式:`SprintfMode` 通过 `fmt.Sprintf` 替换参数,`ReplaceMode` 则使用 `strings.Replacer` 替换 7 | 8 | ## 快速使用 9 | 10 | ```text 11 | lang/ 12 | en/ 13 | default.ini 14 | ... 15 | zh-CN/ 16 | default.ini 17 | ... 18 | ``` 19 | 20 | ### 初始化 21 | 22 | ```go 23 | import "github.com/abulo/ratel/v3/i18n" 24 | 25 | languages := map[string]string{ 26 | "en": "English", 27 | "zh-CN": "简体中文", 28 | // "zh-TW": "繁体中文", 29 | } 30 | 31 | // 这里直接初始化的默认实例 32 | i18n.Init("conf/lang", "en", languages) 33 | 34 | // 或者创建自定义的新实例 35 | // i18n.New(langDir string, defLang string, languages) 36 | // i18n.NewEmpty() 37 | ``` 38 | 39 | 40 | ### 翻译数据 41 | 42 | ```go 43 | // 从指定的语言翻译 44 | msg := i18n.Tr("en", "key") 45 | 46 | // 从默认语言翻译 47 | msg = i18n.DefTr("key") 48 | // with arguments. 49 | msg = i18n.DefTr("key1", "arg1", "arg2") 50 | ``` 51 | 52 | 53 | ### 参数替换模式 54 | 55 | 使用 `SprintfMode`(**defaults**) 模式: 56 | 57 | ```ini 58 | # en.ini 59 | desc = I am %s, age is %d 60 | ``` 61 | 62 | 按kv顺序传入参数使用: 63 | 64 | ```go 65 | msg := i18n.Tr("en", "desc", "name", "tom", "age", 22) 66 | // Output: "I am tom, age is 22" 67 | ``` 68 | 69 | 使用 `ReplaceMode` 替换模式: 70 | 71 | ```ini 72 | # en.ini 73 | desc = I am {name}, age is {age} 74 | ``` 75 | 76 | 传入 `map[string]interface{}` 参数使用: 77 | 78 | ```go 79 | i18n.TransMode = i18n.ReplaceMode 80 | 81 | msg := i18n.Tr("en", "desc", "desc", map[string]interface{}{ 82 | "name": "tom", 83 | "age": 22, 84 | }) 85 | // Output: "I am tom, age is 22" 86 | ``` -------------------------------------------------------------------------------- /lang/lang.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | // Placeholder is a placeholder object that can be used globally. 4 | var Placeholder PlaceholderType 5 | 6 | // AnyType ... 7 | type ( 8 | // AnyType can be used to hold any type. 9 | AnyType = any 10 | // PlaceholderType represents a placeholder type. 11 | PlaceholderType = struct{} 12 | ) 13 | -------------------------------------------------------------------------------- /mod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #拉取缺少的模块,移除不用的模块。 4 | go mod tidy; 5 | #下载依赖包 6 | go mod download; 7 | #打印模块依赖图 8 | go mod graph; 9 | #将依赖复制到vendor下 10 | # go mod vendor; 11 | #校验依赖 12 | go mod verify; 13 | #解释为什么需要依赖 14 | go mod why; 15 | #依赖详情 16 | # go list -m -json all; 17 | -------------------------------------------------------------------------------- /nlpword/ac.go: -------------------------------------------------------------------------------- 1 | package nlpword 2 | 3 | // ac 自动机 4 | type ac struct { 5 | results []string 6 | } 7 | 8 | func (ac *ac) fail(node *Node, c rune) *Node { 9 | var next *Node 10 | for { 11 | next = ac.next(node.Failure, c) 12 | if next == nil { 13 | if node.IsRootNode() { 14 | return node 15 | } 16 | node = node.Failure 17 | continue 18 | } 19 | 20 | return next 21 | } 22 | 23 | } 24 | 25 | func (ac *ac) next(node *Node, c rune) *Node { 26 | next, ok := node.Children[c] 27 | if ok { 28 | return next 29 | } 30 | return nil 31 | } 32 | 33 | func (ac *ac) output(node *Node, runes []rune, position int) { 34 | if node.IsRootNode() { 35 | return 36 | } 37 | 38 | if node.IsPathEnd() { 39 | ac.results = append(ac.results, string(runes[position+1-node.depth:position+1])) 40 | } 41 | 42 | ac.output(node.Failure, runes, position) 43 | } 44 | 45 | func (ac *ac) firstOutput(node *Node, runes []rune, position int) string { 46 | if node.IsRootNode() { 47 | return "" 48 | } 49 | 50 | if node.IsPathEnd() { 51 | return string(runes[position+1-node.depth : position+1]) 52 | } 53 | 54 | return ac.firstOutput(node.Failure, runes, position) 55 | } 56 | 57 | func (ac *ac) replace(node *Node, runes []rune, position int, replace rune) { 58 | if node.IsRootNode() { 59 | return 60 | } 61 | 62 | if node.IsPathEnd() { 63 | for i := position + 1 - node.depth; i < position+1; i++ { 64 | runes[i] = replace 65 | } 66 | } 67 | ac.replace(node.Failure, runes, position, replace) 68 | } 69 | -------------------------------------------------------------------------------- /nlpword/link.go: -------------------------------------------------------------------------------- 1 | package nlpword 2 | 3 | // LinkList ... 4 | type LinkList struct { 5 | head *listNode 6 | tail *listNode 7 | count int64 8 | } 9 | 10 | // Push appends a node 11 | func (list *LinkList) Push(v any) { 12 | node := &listNode{ 13 | Value: v, 14 | } 15 | if list.head == nil { 16 | list.head = node 17 | } else { 18 | list.tail.Next = node 19 | 20 | } 21 | list.tail = node 22 | list.count++ 23 | } 24 | 25 | // Pop returns the value of the first node 26 | func (list *LinkList) Pop() any { 27 | if list.Empty() { 28 | return nil 29 | } 30 | 31 | n := list.head 32 | list.head = n.Next 33 | list.count-- 34 | return n.Value 35 | } 36 | 37 | // Empty returns true if there is none node 38 | func (list *LinkList) Empty() bool { 39 | return list.count == 0 40 | } 41 | 42 | type listNode struct { 43 | Value any 44 | Next *listNode 45 | } 46 | -------------------------------------------------------------------------------- /nlpword/link_test.go: -------------------------------------------------------------------------------- 1 | package nlpword 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestLinkList_Push(t *testing.T) { 9 | type args struct { 10 | v any 11 | } 12 | tests := []struct { 13 | name string 14 | list *LinkList 15 | args args 16 | }{ 17 | // TODO: Add test cases. 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | tt.list.Push(tt.args.v) 22 | }) 23 | } 24 | } 25 | 26 | func TestLinkList_Pop(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | list *LinkList 30 | want any 31 | }{ 32 | // TODO: Add test cases. 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := tt.list.Pop(); !reflect.DeepEqual(got, tt.want) { 37 | t.Errorf("LinkList.Pop() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestLinkList_Empty(t *testing.T) { 44 | tests := []struct { 45 | name string 46 | list *LinkList 47 | want bool 48 | }{ 49 | // TODO: Add test cases. 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | if got := tt.list.Empty(); got != tt.want { 54 | t.Errorf("LinkList.Empty() = %v, want %v", got, tt.want) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /nlpword/readme.md: -------------------------------------------------------------------------------- 1 | 敏感词查找,验证,过滤和替换 2 | 3 | # 4 | 5 | 6 | Usage: 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "github.com/abulo/ratel/v3/nlpword" 14 | ) 15 | 16 | func main() { 17 | filter := nlpword.New() 18 | filter.LoadWordDict("path/to/dict") 19 | filter.LoadNetWordDict("https://raw.githubusercontent.com/importcjj/sensitive/master/dict/dict.txt") 20 | filter.AddWord("长者") 21 | 22 | fmt.Println(filter.Filter("我为长者续一秒")) // 我为续一秒 23 | fmt.Println(filter.Replace("我为长者续一秒", '*')) // 我为**续一秒 24 | fmt.Println(filter.FindIn("我为长者续一秒")) // true, 长者 25 | fmt.Println(filter.Validate("我为长者续一秒")) // False, 长者 26 | fmt.Println(filter.FindAll("我为长者续一秒")) // [长者] 27 | 28 | fmt.Println(filter.FindIn("我为长x者续一秒")) // false 29 | filter.UpdateNoisePattern(`x`) 30 | fmt.Println(filter.FindIn("我为长x者续一秒")) // true, 长者 31 | fmt.Println(filter.Validate("我为长x者续一秒")) // False, 长者 32 | } 33 | ``` -------------------------------------------------------------------------------- /registry/config.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/abulo/ratel/v3/core/logger" 7 | ) 8 | 9 | var registryBuilder = make(map[string]Builder) 10 | 11 | // Config ... 12 | type Config map[string]ConfigLab 13 | 14 | // ConfigLab ... 15 | type ConfigLab struct { 16 | Kind string `json:"kind" description:"底层注册器类型, eg: etcdv3, consul"` 17 | ConfigKey string `json:"configKey" description:"底册注册器的配置键"` 18 | DeplaySeconds int `json:"deplaySeconds" description:"延迟注册"` 19 | } 20 | 21 | // DefaultRegisterer default register 22 | var DefaultRegisterer Registry = &Local{} 23 | 24 | // Builder ... 25 | type Builder func(string) Registry 26 | 27 | // BuildFunc ... 28 | type BuildFunc func(string) (Registry, error) 29 | 30 | // RegisterBuilder ... 31 | func RegisterBuilder(kind string, build Builder) { 32 | if _, ok := registryBuilder[kind]; ok { 33 | log.Panicf("duplicate register registry builder: %s", kind) 34 | } 35 | registryBuilder[kind] = build 36 | } 37 | 38 | // New ... 39 | func New() Config { 40 | var config Config 41 | return config 42 | } 43 | 44 | // Lab ... 45 | func (config Config) Lab(name string, lab ConfigLab) Config { 46 | config["ddd"] = lab 47 | return config 48 | } 49 | 50 | // InitDefaultRegister ... 51 | func (config Config) InitDefaultRegister() { 52 | for name, item := range config { 53 | var itemKind = item.Kind 54 | if itemKind == "" { 55 | itemKind = "etcdv3" 56 | } 57 | build, ok := registryBuilder[itemKind] 58 | if !ok { 59 | logger.Logger.Printf("invalid registry kind: %s", itemKind) 60 | continue 61 | } 62 | DefaultRegisterer = build(item.ConfigKey) 63 | logger.Logger.Printf("build registry %s with config: %s", name, item.ConfigKey) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /registry/config_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRegisterBuilder(t *testing.T) { 9 | type args struct { 10 | kind string 11 | build Builder 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | }{ 17 | // TODO: Add test cases. 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | RegisterBuilder(tt.args.kind, tt.args.build) 22 | }) 23 | } 24 | } 25 | 26 | func TestNew(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | want Config 30 | }{ 31 | // TODO: Add test cases. 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if got := New(); !reflect.DeepEqual(got, tt.want) { 36 | t.Errorf("New() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestConfig_Lab(t *testing.T) { 43 | type args struct { 44 | name string 45 | lab ConfigLab 46 | } 47 | tests := []struct { 48 | name string 49 | config Config 50 | args args 51 | want Config 52 | }{ 53 | // TODO: Add test cases. 54 | } 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | if got := tt.config.Lab(tt.args.name, tt.args.lab); !reflect.DeepEqual(got, tt.want) { 58 | t.Errorf("Config.Lab() = %v, want %v", got, tt.want) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestConfig_InitDefaultRegister(t *testing.T) { 65 | tests := []struct { 66 | name string 67 | config Config 68 | }{ 69 | // TODO: Add test cases. 70 | } 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | tt.config.InitDefaultRegister() 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /registry/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_newEndpoints(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | want *Endpoints 12 | }{ 13 | // TODO: Add test cases. 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | if got := newEndpoints(); !reflect.DeepEqual(got, tt.want) { 18 | t.Errorf("newEndpoints() = %v, want %v", got, tt.want) 19 | } 20 | }) 21 | } 22 | } 23 | 24 | func TestEndpoints_DeepCopy(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | in *Endpoints 28 | want *Endpoints 29 | }{ 30 | // TODO: Add test cases. 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := tt.in.DeepCopy(); !reflect.DeepEqual(got, tt.want) { 35 | t.Errorf("Endpoints.DeepCopy() = %v, want %v", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestEndpoints_DeepCopyInfo(t *testing.T) { 42 | type args struct { 43 | out *Endpoints 44 | } 45 | tests := []struct { 46 | name string 47 | in *Endpoints 48 | args args 49 | }{ 50 | // TODO: Add test cases. 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | tt.in.DeepCopyInfo(tt.args.out) 55 | }) 56 | } 57 | } 58 | 59 | func TestRouteConfig_String(t *testing.T) { 60 | tests := []struct { 61 | name string 62 | config RouteConfig 63 | want string 64 | }{ 65 | // TODO: Add test cases. 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | if got := tt.config.String(); got != tt.want { 70 | t.Errorf("RouteConfig.String() = %v, want %v", got, tt.want) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /registry/etcdv3/config.go: -------------------------------------------------------------------------------- 1 | package etcdv3 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/abulo/ratel/v3/client/etcdv3" 7 | "github.com/abulo/ratel/v3/core/constant" 8 | "github.com/abulo/ratel/v3/core/logger" 9 | "github.com/abulo/ratel/v3/core/singleton" 10 | "github.com/abulo/ratel/v3/registry" 11 | ) 12 | 13 | // Config ... 14 | type Config struct { 15 | *etcdv3.Config 16 | ReadTimeout time.Duration 17 | Node string 18 | Prefix string 19 | ServiceTTL time.Duration 20 | } 21 | 22 | // New ... 23 | func New() *Config { 24 | return &Config{ 25 | Config: etcdv3.New(), 26 | ReadTimeout: time.Second * 3, 27 | Prefix: "ratel", 28 | ServiceTTL: 0, 29 | } 30 | } 31 | 32 | // SetNode ... 33 | func (config *Config) SetNode(prefix string) *Config { 34 | config.Node = prefix 35 | return config 36 | } 37 | 38 | func (config *Config) GetNode() string { 39 | return config.Node 40 | } 41 | 42 | // SetPrefix ... 43 | func (config *Config) SetPrefix(prefix string) *Config { 44 | config.Prefix = prefix 45 | return config 46 | } 47 | 48 | // Build ... 49 | func (config *Config) Build() (registry.Registry, error) { 50 | return newETCDRegistry(config) 51 | } 52 | 53 | // MustBuild ... 54 | func (config *Config) MustBuild() registry.Registry { 55 | reg, err := config.Build() 56 | if err != nil { 57 | logger.Logger.Panicf("build registry failed: %v", err) 58 | } 59 | return reg 60 | } 61 | 62 | func (config *Config) Singleton() (registry.Registry, error) { 63 | if val, ok := singleton.Load(constant.ModuleClientEtcd, config.Node); ok { 64 | return val.(registry.Registry), nil 65 | } 66 | 67 | reg, err := config.Build() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | singleton.Store(constant.ModuleClientEtcd, config.Node, reg) 73 | 74 | return reg, nil 75 | } 76 | 77 | func (config *Config) MustSingleton() registry.Registry { 78 | reg, err := config.Singleton() 79 | if err != nil { 80 | logger.Logger.Panicf("build registry failed: %v", err) 81 | } 82 | 83 | return reg 84 | } 85 | -------------------------------------------------------------------------------- /registry/nop.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/core/logger" 7 | "github.com/abulo/ratel/v3/server" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Local Nop registry, used for local development/debugging 12 | type Local struct{} 13 | 14 | // ListServices ... 15 | func (n Local) ListServices(ctx context.Context, s string) ([]*server.ServiceInfo, error) { 16 | panic("implement me") 17 | } 18 | 19 | // WatchServices ... 20 | func (n Local) WatchServices(ctx context.Context, s string) (chan Endpoints, error) { 21 | panic("implement me") 22 | } 23 | 24 | // RegisterService ... 25 | func (n Local) RegisterService(ctx context.Context, si *server.ServiceInfo) error { 26 | 27 | logger.Logger.WithFields(logrus.Fields{ 28 | "action": "registry", 29 | "name": si.Name, 30 | "label": si.Label(), 31 | }).Info("register service locally") 32 | 33 | return nil 34 | } 35 | 36 | // UnregisterService ... 37 | func (n Local) UnregisterService(ctx context.Context, si *server.ServiceInfo) error { 38 | logger.Logger.WithFields(logrus.Fields{ 39 | "action": "registry", 40 | "name": si.Name, 41 | "label": si.Label(), 42 | }).Info("unregister service locally") 43 | return nil 44 | } 45 | 46 | // Close ... 47 | func (n Local) Close() error { return nil } 48 | 49 | // Kind Close ... 50 | func (n Local) Kind() string { return "local" } 51 | -------------------------------------------------------------------------------- /registry/service.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import "github.com/abulo/ratel/v3/server" 4 | 5 | type service struct { 6 | server.Server 7 | } 8 | 9 | // Service ... 10 | func Service(srv server.Server) server.Server { 11 | return &service{Server: srv} 12 | } 13 | 14 | // Serve ... 15 | func (s *service) Serve() error { 16 | 17 | return s.Server.Serve() 18 | } 19 | -------------------------------------------------------------------------------- /registry/service_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/abulo/ratel/v3/server" 8 | ) 9 | 10 | func TestService(t *testing.T) { 11 | type args struct { 12 | srv server.Server 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want server.Server 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if got := Service(tt.args.srv); !reflect.DeepEqual(got, tt.want) { 24 | t.Errorf("Service() = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func Test_service_Serve(t *testing.T) { 31 | tests := []struct { 32 | name string 33 | s *service 34 | wantErr bool 35 | }{ 36 | // TODO: Add test cases. 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | if err := tt.s.Serve(); (err != nil) != tt.wantErr { 41 | t.Errorf("service.Serve() error = %v, wantErr %v", err, tt.wantErr) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /registry/update.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import "github.com/abulo/ratel/v3/server" 4 | 5 | type Operation uint8 6 | 7 | const ( 8 | // Add indicates an Endpoint is added. 9 | Add Operation = iota 10 | // Delete indicates an existing address is deleted. 11 | Delete 12 | ) 13 | 14 | // Update defines a name resolution update. Notice that it is not valid having both 15 | // empty string Addr and nil Metadata in an Update. 16 | type Update struct { 17 | // Op indicates the operation of the update. 18 | Op Operation 19 | // Addr is the updated address. It is empty string if there is no address update. 20 | Addr string 21 | // Metadata is the updated metadata. It is nil if there is no metadata update. 22 | // Metadata is not required for a custom naming implementation. 23 | Metadata *server.ServiceInfo 24 | } 25 | -------------------------------------------------------------------------------- /server/xgin/const_test.go: -------------------------------------------------------------------------------- 1 | package xgin 2 | 3 | import "testing" 4 | 5 | func TestStatusText(t *testing.T) { 6 | type args struct { 7 | code int 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := StatusText(tt.args.code); got != tt.want { 19 | t.Errorf("StatusText() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/xgin/error.go: -------------------------------------------------------------------------------- 1 | package xgin 2 | 3 | import ( 4 | "google.golang.org/grpc/codes" 5 | "google.golang.org/grpc/status" 6 | ) 7 | 8 | var ( 9 | // errBadRequest = status.Errorf(codes.InvalidArgument, createStatusErr(codeMSInvalidParam, "bad request")) 10 | errMicroDefault = status.Errorf(codes.Internal, createStatusErr(codeMS, "micro default")) 11 | // errMicroInvoke = status.Errorf(codes.Internal, createStatusErr(codeMSInvoke, "invoke failed")) 12 | // errMicroInvokeLen = status.Errorf(codes.Internal, createStatusErr(codeMSInvokeLen, "invoke result not 2 item")) 13 | // errMicroInvokeInvalid = status.Errorf(codes.Internal, createStatusErr(codeMSSecondItemNotError, "second invoke res not a error")) 14 | // errMicroResInvalid = status.Errorf(codes.Internal, createStatusErr(codeMSResErr, "response is not valid")) 15 | ) 16 | 17 | // HTTPError wraps handler error. 18 | type HTTPError struct { 19 | Code int 20 | Message string 21 | } 22 | 23 | // NewHTTPError constructs a new HTTPError instance. 24 | func NewHTTPError(code int, msg ...string) *HTTPError { 25 | he := &HTTPError{Code: code, Message: StatusText(code)} 26 | if len(msg) > 0 { 27 | he.Message = msg[0] 28 | } 29 | 30 | return he 31 | } 32 | 33 | // Errord return error message. 34 | func (e HTTPError) Error() string { 35 | return e.Message 36 | } 37 | 38 | // ErrNotFound defines StatusNotFound error. 39 | var ErrNotFound = HTTPError{ 40 | Code: StatusNotFound, 41 | Message: "not found", 42 | } 43 | 44 | // ErrGRPCResponseValid ... 45 | var ( 46 | // ErrGRPCResponseValid ... 47 | ErrGRPCResponseValid = status.Errorf(codes.Internal, "response valid") 48 | // ErrGRPCInvokeLen ... 49 | ErrGRPCInvokeLen = status.Errorf(codes.Internal, "invoke request without len 2 res") 50 | ) 51 | -------------------------------------------------------------------------------- /server/xgin/error_test.go: -------------------------------------------------------------------------------- 1 | package xgin 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewHTTPError(t *testing.T) { 9 | type args struct { 10 | code int 11 | msg []string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *HTTPError 17 | }{ 18 | // TODO: Add test cases. 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if got := NewHTTPError(tt.args.code, tt.args.msg...); !reflect.DeepEqual(got, tt.want) { 23 | t.Errorf("NewHTTPError() = %v, want %v", got, tt.want) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestHTTPError_Error(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | e HTTPError 33 | want string 34 | }{ 35 | // TODO: Add test cases. 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := tt.e.Error(); got != tt.want { 40 | t.Errorf("HTTPError.Error() = %v, want %v", got, tt.want) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/xgin/websocket.go: -------------------------------------------------------------------------------- 1 | package xgin 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/abulo/ratel/v3/core/logger" 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | // WebSocketConn websocket conn, see websocket.Conn 14 | type WebSocketConn interface { 15 | Subprotocol() string 16 | Close() error 17 | LocalAddr() net.Addr 18 | RemoteAddr() net.Addr 19 | WriteControl(messageType int, data []byte, deadline time.Time) error 20 | NextWriter(messageType int) (io.WriteCloser, error) 21 | WritePreparedMessage(pm *websocket.PreparedMessage) error 22 | WriteMessage(messageType int, data []byte) error 23 | SetWriteDeadline(t time.Time) error 24 | NextReader() (messageType int, r io.Reader, err error) 25 | ReadMessage() (messageType int, p []byte, err error) 26 | SetReadDeadline(t time.Time) error 27 | SetReadLimit(limit int64) 28 | CloseHandler() func(code int, text string) error 29 | SetCloseHandler(h func(code int, text string) error) 30 | PingHandler() func(appData string) error 31 | SetPingHandler(h func(appData string) error) 32 | PongHandler() func(appData string) error 33 | SetPongHandler(h func(appData string) error) 34 | UnderlyingConn() net.Conn 35 | EnableWriteCompression(enable bool) 36 | SetCompressionLevel(level int) error 37 | } 38 | 39 | // WebSocketFunc .. 40 | type WebSocketFunc func(WebSocketConn, error) 41 | 42 | // WebSocket .. 43 | type WebSocket struct { 44 | Pattern string 45 | Handler WebSocketFunc 46 | *websocket.Upgrader 47 | Header http.Header 48 | } 49 | 50 | // Upgrade get upgrage request 51 | func (ws *WebSocket) Upgrade(w http.ResponseWriter, r *http.Request) { 52 | conn, err := ws.Upgrader.Upgrade(w, r, ws.Header) 53 | if err == nil { 54 | defer func() { 55 | if err := conn.Close(); err != nil { 56 | logger.Logger.Error("Error closing conn: ", err) 57 | } 58 | }() 59 | } 60 | ws.Handler(conn, err) 61 | } 62 | 63 | // WebSocketOption .. 64 | type WebSocketOption func(*WebSocket) 65 | 66 | // WebSocketOptions .. 67 | func WebSocketOptions(pattern string, handler WebSocketFunc, opts ...WebSocketOption) *WebSocket { 68 | ws := &WebSocket{ 69 | Pattern: pattern, 70 | Handler: handler, 71 | Upgrader: &websocket.Upgrader{}, 72 | } 73 | for _, opt := range opts { 74 | opt(ws) 75 | } 76 | return ws 77 | } 78 | -------------------------------------------------------------------------------- /server/xgin/websocket_test.go: -------------------------------------------------------------------------------- 1 | package xgin 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestWebSocket_Upgrade(t *testing.T) { 10 | type args struct { 11 | w http.ResponseWriter 12 | r *http.Request 13 | } 14 | tests := []struct { 15 | name string 16 | ws *WebSocket 17 | args args 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | tt.ws.Upgrade(tt.args.w, tt.args.r) 24 | }) 25 | } 26 | } 27 | 28 | func TestWebSocketOptions(t *testing.T) { 29 | type args struct { 30 | pattern string 31 | handler WebSocketFunc 32 | opts []WebSocketOption 33 | } 34 | tests := []struct { 35 | name string 36 | args args 37 | want *WebSocket 38 | }{ 39 | // TODO: Add test cases. 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | if got := WebSocketOptions(tt.args.pattern, tt.args.handler, tt.args.opts...); !reflect.DeepEqual(got, tt.want) { 44 | t.Errorf("WebSocketOptions() = %v, want %v", got, tt.want) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/xgrpc/recovery/interceptors.go: -------------------------------------------------------------------------------- 1 | package recovery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | // RecoveryHandlerFunc is a function that recovers from the panic `p` by returning an `error`. 12 | type RecoveryHandlerFunc func(p any) (err error) 13 | 14 | // RecoveryHandlerFuncContext is a function that recovers from the panic `p` by returning an `error`. 15 | // The context can be used to extract request scoped metadata and context values. 16 | type RecoveryHandlerFuncContext func(ctx context.Context, p any) (err error) 17 | 18 | // UnaryServerInterceptor returns a new unary server interceptor for panic recovery. 19 | func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor { 20 | o := evaluateOptions(opts) 21 | return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ any, err error) { 22 | defer func() { 23 | if r := recover(); r != nil { 24 | err = recoverFrom(ctx, r, o.recoveryHandlerFunc) 25 | } 26 | }() 27 | 28 | return handler(ctx, req) 29 | } 30 | } 31 | 32 | // StreamServerInterceptor returns a new streaming server interceptor for panic recovery. 33 | func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor { 34 | o := evaluateOptions(opts) 35 | return func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) { 36 | defer func() { 37 | if r := recover(); r != nil { 38 | err = recoverFrom(stream.Context(), r, o.recoveryHandlerFunc) 39 | } 40 | }() 41 | 42 | return handler(srv, stream) 43 | } 44 | } 45 | 46 | func recoverFrom(ctx context.Context, p any, r RecoveryHandlerFuncContext) error { 47 | if r != nil { 48 | return r(ctx, p) 49 | } 50 | stack := make([]byte, 64<<10) 51 | stack = stack[:runtime.Stack(stack, false)] 52 | return &PanicError{Panic: p, Stack: stack} 53 | } 54 | 55 | type PanicError struct { 56 | Panic any 57 | Stack []byte 58 | } 59 | 60 | func (e *PanicError) Error() string { 61 | return fmt.Sprintf("panic caught: %v\n\n%s", e.Panic, e.Stack) 62 | } 63 | -------------------------------------------------------------------------------- /server/xgrpc/recovery/options.go: -------------------------------------------------------------------------------- 1 | package recovery 2 | 3 | import "context" 4 | 5 | var ( 6 | defaultOptions = &options{ 7 | recoveryHandlerFunc: nil, 8 | } 9 | ) 10 | 11 | type options struct { 12 | recoveryHandlerFunc RecoveryHandlerFuncContext 13 | } 14 | 15 | func evaluateOptions(opts []Option) *options { 16 | optCopy := &options{} 17 | *optCopy = *defaultOptions 18 | for _, o := range opts { 19 | o(optCopy) 20 | } 21 | return optCopy 22 | } 23 | 24 | type Option func(*options) 25 | 26 | // WithRecoveryHandler customizes the function for recovering from a panic. 27 | func WithRecoveryHandler(f RecoveryHandlerFunc) Option { 28 | return func(o *options) { 29 | o.recoveryHandlerFunc = RecoveryHandlerFuncContext(func(ctx context.Context, p any) error { 30 | return f(p) 31 | }) 32 | } 33 | } 34 | 35 | // WithRecoveryHandlerContext customizes the function for recovering from a panic. 36 | func WithRecoveryHandlerContext(f RecoveryHandlerFuncContext) Option { 37 | return func(o *options) { 38 | o.recoveryHandlerFunc = f 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/xhertz/server.go: -------------------------------------------------------------------------------- 1 | package xhertz 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/abulo/ratel/v3/core/constant" 7 | "github.com/abulo/ratel/v3/server" 8 | hserver "github.com/cloudwego/hertz/pkg/app/server" 9 | ) 10 | 11 | // Server ... 12 | type Server struct { 13 | *hserver.Hertz 14 | config *Config 15 | } 16 | 17 | func newServer(config *Config) *Server { 18 | return &Server{ 19 | Hertz: hserver.New( 20 | hserver.WithHostPorts(config.Address()), 21 | hserver.WithDisablePrintRoute(config.Mode == ReleaseMode), 22 | hserver.WithMaxRequestBodySize(20*1204*1204), 23 | hserver.WithMaxKeepBodySize(20*1204*1204), 24 | hserver.WithReadBufferSize(20*1204*1204), 25 | ), 26 | config: config, 27 | } 28 | } 29 | 30 | // Serve implements server.Server interface. 31 | func (s *Server) Serve() error { 32 | return s.Run() 33 | // return 34 | } 35 | 36 | // Stop implements server.Server interface 37 | // it will terminate gin server immediately 38 | func (s *Server) Stop() error { 39 | return s.Close() 40 | } 41 | 42 | // GracefulStop implements server.Server interface 43 | // it will stop gin server gracefully 44 | func (s *Server) GracefulStop(ctx context.Context) error { 45 | return s.Shutdown(ctx) 46 | } 47 | 48 | // Info returns server info, used by governor and consumer balancer 49 | func (s *Server) Info() *server.ServiceInfo { 50 | serviceAddr := s.config.Address() 51 | if s.config.ServiceAddress != "" { 52 | serviceAddr = s.config.ServiceAddress 53 | } 54 | info := server.ApplyOptions( 55 | server.WithScheme("http"), 56 | server.WithAddress(serviceAddr), 57 | server.WithKind(constant.ServiceProvider), 58 | ) 59 | return &info 60 | } 61 | 62 | // Health ... 63 | func (s *Server) Health() bool { 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /server/xmonitor/config.go: -------------------------------------------------------------------------------- 1 | package xmonitor 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ModName .. 8 | const ModName = "server.monitor" 9 | 10 | // Config HTTP config 11 | type Config struct { 12 | Host string 13 | Port int 14 | Deployment string 15 | Mode string 16 | ServiceAddress string // ServiceAddress service address in registry info, default to 'Host:Port' 17 | } 18 | 19 | // New ... 20 | func New() *Config { 21 | return &Config{} 22 | } 23 | 24 | // WithHost ... 25 | func (config *Config) WithHost(host string) *Config { 26 | config.Host = host 27 | return config 28 | } 29 | 30 | // WithPort ... 31 | func (config *Config) WithPort(port int) *Config { 32 | config.Port = port 33 | return config 34 | } 35 | 36 | // WithDeployment ... 37 | func (config *Config) WithDeployment(deployment string) *Config { 38 | config.Deployment = deployment 39 | return config 40 | } 41 | 42 | // WithMode ... 43 | func (config *Config) WithMode(mode string) *Config { 44 | config.Mode = mode 45 | return config 46 | } 47 | 48 | // WithServiceAddress ... 49 | func (config *Config) WithServiceAddress(serviceAddress string) *Config { 50 | config.ServiceAddress = serviceAddress 51 | return config 52 | } 53 | 54 | // Build ... 55 | func (config *Config) Build() *Server { 56 | return newServer(config) 57 | } 58 | 59 | // Address ... 60 | func (config Config) Address() string { 61 | return fmt.Sprintf("%s:%d", config.Host, config.Port) 62 | } 63 | -------------------------------------------------------------------------------- /server/xmonitor/init.go: -------------------------------------------------------------------------------- 1 | package xmonitor 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/pprof" 7 | "runtime/debug" 8 | ) 9 | 10 | // DefaultServeMux ... 11 | var ( 12 | // DefaultServeMux ... 13 | DefaultServeMux = http.NewServeMux() 14 | routes = []string{} 15 | ) 16 | 17 | // InitHandle ... 18 | func (s *Server) InitHandle() { 19 | // 获取全部治理路由 20 | s.HandleFunc("/routes", func(resp http.ResponseWriter, req *http.Request) { 21 | _ = json.NewEncoder(resp).Encode(routes) 22 | }) 23 | s.HandleFunc("/debug/pprof/", pprof.Index) 24 | s.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 25 | s.HandleFunc("/debug/pprof/profile", pprof.Profile) 26 | s.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 27 | s.HandleFunc("/debug/pprof/trace", pprof.Trace) 28 | 29 | if info, ok := debug.ReadBuildInfo(); ok { 30 | s.HandleFunc("/modInfo", func(w http.ResponseWriter, r *http.Request) { 31 | encoder := json.NewEncoder(w) 32 | if r.URL.Query().Get("pretty") == "true" { 33 | encoder.SetIndent("", " ") 34 | } 35 | _ = encoder.Encode(info) 36 | }) 37 | } 38 | } 39 | 40 | // HandleFunc ... 41 | func (s *Server) HandleFunc(pattern string, handler http.HandlerFunc) { 42 | DefaultServeMux.HandleFunc(pattern, handler) 43 | routes = append(routes, pattern) 44 | } 45 | -------------------------------------------------------------------------------- /server/xmonitor/init_test.go: -------------------------------------------------------------------------------- 1 | package xmonitor 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestServer_InitHandle(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | s *Server 12 | }{ 13 | // TODO: Add test cases. 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | tt.s.InitHandle() 18 | }) 19 | } 20 | } 21 | 22 | func TestServer_HandleFunc(t *testing.T) { 23 | type args struct { 24 | pattern string 25 | handler http.HandlerFunc 26 | } 27 | tests := []struct { 28 | name string 29 | s *Server 30 | args args 31 | }{ 32 | // TODO: Add test cases. 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | tt.s.HandleFunc(tt.args.pattern, tt.args.handler) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/xmonitor/server.go: -------------------------------------------------------------------------------- 1 | package xmonitor 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/abulo/ratel/v3/core/constant" 9 | "github.com/abulo/ratel/v3/core/logger" 10 | "github.com/abulo/ratel/v3/server" 11 | ) 12 | 13 | // Server ... 14 | type Server struct { 15 | *http.Server 16 | listener net.Listener 17 | *Config 18 | } 19 | 20 | func newServer(config *Config) *Server { 21 | var listener, err = net.Listen("tcp4", config.Address()) 22 | if err != nil { 23 | logger.Logger.Panic("start error:", err) 24 | } 25 | 26 | return &Server{ 27 | Server: &http.Server{ 28 | Addr: config.Address(), 29 | Handler: DefaultServeMux, 30 | }, 31 | listener: listener, 32 | Config: config, 33 | } 34 | } 35 | 36 | // Serve .. 37 | func (s *Server) Serve() error { 38 | err := s.Server.Serve(s.listener) 39 | if err == http.ErrServerClosed { 40 | return nil 41 | } 42 | return err 43 | 44 | } 45 | 46 | // Stop .. 47 | func (s *Server) Stop() error { 48 | return s.Server.Close() 49 | } 50 | 51 | // GracefulStop .. 52 | func (s *Server) GracefulStop(ctx context.Context) error { 53 | return s.Server.Shutdown(ctx) 54 | } 55 | 56 | // Health 57 | // TODO(roamerlv): 58 | func (s *Server) Health() bool { 59 | return true 60 | } 61 | 62 | // Info .. 63 | func (s *Server) Info() *server.ServiceInfo { 64 | serviceAddr := s.listener.Addr().String() 65 | if s.Config.ServiceAddress != "" { 66 | serviceAddr = s.Config.ServiceAddress 67 | } 68 | 69 | info := server.ApplyOptions( 70 | server.WithScheme("http"), 71 | server.WithAddress(serviceAddr), 72 | server.WithKind(constant.ServiceMonitor), 73 | ) 74 | // info.Name = info.Name + "." + ModName 75 | return &info 76 | } 77 | -------------------------------------------------------------------------------- /snowflake/readme.md: -------------------------------------------------------------------------------- 1 | ## Quick Start 2 | Generate an ID 3 | ```go 4 | 5 | import ( 6 | "fmt" 7 | "github.com/abulo/ratel/v3/snowflake" 8 | ) 9 | 10 | func ExampleGenInt64ID() { 11 | id := snowflake.CommonConfig.GenInt64ID() 12 | fmt.Printf("id generated: %v", id) 13 | } 14 | ``` -------------------------------------------------------------------------------- /stores/elasticsearch/elastic_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestNewClient(t *testing.T) { 10 | type args struct { 11 | config *Config 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *Client 17 | }{ 18 | // TODO: Add test cases. 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if got := NewClient(tt.args.config); !reflect.DeepEqual(got, tt.want) { 23 | t.Errorf("NewClient() = %v, want %v", got, tt.want) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestESTraceServerInterceptor(t *testing.T) { 30 | type args struct { 31 | DisableMetric bool 32 | DisableTrace bool 33 | Addr string 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want *http.Client 39 | }{ 40 | // TODO: Add test cases. 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if got := ESTraceServerInterceptor(tt.args.DisableMetric, tt.args.DisableTrace, tt.args.Addr); !reflect.DeepEqual(got, tt.want) { 45 | t.Errorf("ESTraceServerInterceptor() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestESTracedTransport_RoundTrip(t *testing.T) { 52 | type args struct { 53 | r *http.Request 54 | } 55 | tests := []struct { 56 | name string 57 | tr *ESTracedTransport 58 | args args 59 | wantResp *http.Response 60 | wantErr bool 61 | }{ 62 | // TODO: Add test cases. 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | gotResp, err := tt.tr.RoundTrip(tt.args.r) 67 | if (err != nil) != tt.wantErr { 68 | t.Errorf("ESTracedTransport.RoundTrip() error = %v, wantErr %v", err, tt.wantErr) 69 | return 70 | } 71 | if !reflect.DeepEqual(gotResp, tt.wantResp) { 72 | t.Errorf("ESTracedTransport.RoundTrip() = %v, want %v", gotResp, tt.wantResp) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stores/null/format.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | const ( 4 | RFC3339DateOnly = "2006-01-02" 5 | RFC3339TimeOnly = "15:04:05" 6 | DateTimeSQL = "2006-01-02 15:04:05" 7 | TimeStampSQL = "2006-01-02 15:04:05.000000" 8 | ) 9 | -------------------------------------------------------------------------------- /stores/null/value.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | // Value specifies methods that allow introspection into the 4 | // state of a value. 5 | type Value interface { 6 | // IsValid returns true iff the value is set and non-null 7 | IsValid() bool 8 | // IsSet returns true iff the value is set (null inclusive) 9 | IsSet() bool 10 | } 11 | -------------------------------------------------------------------------------- /stores/proxy/elasticsearch.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "github.com/abulo/ratel/v3/stores/elasticsearch" 4 | 5 | // ElasticSearch ... 6 | type ElasticSearch struct { 7 | *elasticsearch.Client 8 | } 9 | 10 | // NewElasticSearch 缓存 11 | func NewElasticSearch() *ElasticSearch { 12 | return &ElasticSearch{} 13 | } 14 | 15 | // Store 设置写库 16 | func (proxy *ElasticSearch) Store(client *elasticsearch.Client) { 17 | proxy.Client = client 18 | } 19 | 20 | // StoreElasticSearch StoreEs 设置组 21 | func (proxyPool *Proxy) StoreElasticSearch(group string, proxy *ElasticSearch) { 22 | proxyPool.m.Store(group, proxy) 23 | } 24 | 25 | // LoadElasticSearch LoadEs 获取分组 26 | func (proxyPool *Proxy) LoadElasticSearch(group string) *elasticsearch.Client { 27 | if f, ok := proxyPool.m.Load(group); ok { 28 | return f.(*ElasticSearch).Client 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /stores/proxy/mongodb.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "github.com/abulo/ratel/v3/stores/mongodb" 4 | 5 | // MongoDB ... 6 | type MongoDB struct { 7 | *mongodb.MongoDB 8 | } 9 | 10 | // NewMongoDB 缓存 11 | func NewMongoDB() *MongoDB { 12 | return &MongoDB{} 13 | } 14 | 15 | // Store 设置写库 16 | func (proxy *MongoDB) Store(client *mongodb.MongoDB) { 17 | proxy.MongoDB = client 18 | } 19 | 20 | // StoreMongoDB StoreNoSQL 设置组 21 | func (proxyPool *Proxy) StoreMongoDB(group string, proxy *MongoDB) { 22 | proxyPool.m.Store(group, proxy) 23 | } 24 | 25 | // LoadMongoDB LoadNoSQL 获取分组 26 | func (proxyPool *Proxy) LoadMongoDB(group string) *mongodb.MongoDB { 27 | if f, ok := proxyPool.m.Load(group); ok { 28 | return f.(*MongoDB).MongoDB 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /stores/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "sync" 4 | 5 | // Proxy 代理池 6 | type Proxy struct { 7 | m sync.Map 8 | } 9 | 10 | // NewProxy 代理池 11 | func NewProxy() *Proxy { 12 | return &Proxy{ 13 | m: sync.Map{}, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stores/proxy/redis.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "github.com/abulo/ratel/v3/stores/redis" 4 | 5 | // Redis ... 6 | type Redis struct { 7 | *redis.Client 8 | } 9 | 10 | // NewRedis 缓存 11 | func NewRedis() *Redis { 12 | return &Redis{} 13 | } 14 | 15 | // Store 设置写库 16 | func (proxy *Redis) Store(client *redis.Client) { 17 | proxy.Client = client 18 | } 19 | 20 | // StoreRedis StoreCache 设置组 21 | func (proxyPool *Proxy) StoreRedis(group string, proxy *Redis) { 22 | proxyPool.m.Store(group, proxy) 23 | } 24 | 25 | // LoadRedis LoadCache 获取分组 26 | func (proxyPool *Proxy) LoadRedis(group string) *redis.Client { 27 | if f, ok := proxyPool.m.Load(group); ok { 28 | return f.(*Redis).Client 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /stores/proxy/sql.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/abulo/ratel/v3/util" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | // SQL Proxy 代理 9 | type SQL struct { 10 | write []*gorm.DB 11 | read []*gorm.DB 12 | } 13 | 14 | // NewSQL 代理池 15 | func NewSQL() *SQL { 16 | return &SQL{} 17 | } 18 | 19 | // SetWrite 设置写库 20 | func (proxy *SQL) SetWrite(db *gorm.DB) { 21 | proxy.write = append(proxy.write, db) 22 | } 23 | 24 | // SetRead 设置读库 25 | func (proxy *SQL) SetRead(db *gorm.DB) { 26 | proxy.read = append(proxy.read, db) 27 | } 28 | 29 | // Write 获取写库 30 | func (proxy *SQL) Write() *gorm.DB { 31 | len := len(proxy.write) 32 | write := util.Rand(0, len-1) 33 | return proxy.write[write] 34 | } 35 | 36 | // Read 获取读库 37 | func (proxy *SQL) Read() *gorm.DB { 38 | len := len(proxy.read) 39 | if len < 1 { 40 | return proxy.Write() 41 | } 42 | read := util.Rand(0, len-1) 43 | return proxy.read[read] 44 | } 45 | 46 | // StoreSQL 设置组 47 | func (proxyPool *Proxy) StoreSQL(group string, proxy *SQL) { 48 | proxyPool.m.Store(group, proxy) 49 | } 50 | 51 | // LoadSQL 获取分组 52 | func (proxyPool *Proxy) LoadSQL(group string) *SQL { 53 | if f, ok := proxyPool.m.Load(group); ok { 54 | return f.(*SQL) 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /stores/redis/hyperloglog_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // PFAdd 将指定元素添加到HyperLogLog key: HyperLogLog键名, els: 要添加的元素列表 8 | func (r *Client) PFAdd(ctx context.Context, key string, els ...any) (val int64, err error) { 9 | err = r.brk.DoWithAcceptable(func() error { 10 | conn, err := getRedis(r) 11 | if err != nil { 12 | return err 13 | } 14 | val, err = conn.PFAdd(getCtx(ctx), key, els...).Result() 15 | return err 16 | }, acceptable) 17 | return 18 | } 19 | 20 | // PFCount 返回HyperlogLog观察到的集合的近似基数。 keys: 要查询的HyperLogLog键名列表 21 | func (r *Client) PFCount(ctx context.Context, keys ...string) (val int64, err error) { 22 | err = r.brk.DoWithAcceptable(func() error { 23 | conn, err := getRedis(r) 24 | if err != nil { 25 | return err 26 | } 27 | val, err = conn.PFCount(getCtx(ctx), keys...).Result() 28 | return err 29 | }, acceptable) 30 | return 31 | } 32 | 33 | // PFMerge N个不同的HyperLogLog合并为一个。 dest: 目标HyperLogLog键名, keys: 要合并的HyperLogLog键名列表 34 | func (r *Client) PFMerge(ctx context.Context, dest string, keys ...string) (val string, err error) { 35 | err = r.brk.DoWithAcceptable(func() error { 36 | conn, err := getRedis(r) 37 | if err != nil { 38 | return err 39 | } 40 | val, err = conn.PFMerge(getCtx(ctx), dest, keys...).Result() 41 | return err 42 | }, acceptable) 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /stores/redis/utils.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | // ks 使用前缀格式化并返回一组键 key: 要格式化的键 10 | // func (r *Client) ks(key ...any) []string { 11 | // keys := make([]string, len(key)) 12 | // for i, k := range key { 13 | // keys[i] = cast.ToString(k) 14 | // } 15 | // return keys 16 | // } 17 | 18 | // acceptable 判断错误是否可接受 err: 要判断的错误 19 | func acceptable(err error) bool { 20 | return err == nil || err == redis.Nil || err == context.Canceled 21 | } 22 | 23 | // getCtx 获取有效的上下文 ctx: 要检查的上下文 24 | func getCtx(ctx context.Context) context.Context { 25 | if ctx == nil || ctx.Err() != nil { 26 | ctx = context.TODO() 27 | } 28 | return ctx 29 | } 30 | -------------------------------------------------------------------------------- /stores/sql/pagination.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | type Pagination struct { 10 | Offset *int64 11 | Limit *int64 12 | } 13 | 14 | func (obj *Pagination) GetOffset() (int64, error) { 15 | if obj.Offset == nil { 16 | return 0, errors.New("offset is nil") 17 | } 18 | return cast.ToInt64(obj.Offset), nil 19 | } 20 | 21 | func (obj *Pagination) GetLimit() (int64, error) { 22 | if obj.Limit == nil { 23 | return 0, errors.New("limit is nil") 24 | } 25 | return cast.ToInt64(obj.Limit), nil 26 | } 27 | -------------------------------------------------------------------------------- /stores/sql/scope.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | type Scope struct { 10 | Start *string 11 | End *string 12 | } 13 | 14 | func (obj *Scope) GetStartString() (string, error) { 15 | if obj.Start == nil { 16 | return "", errors.New("start is nil") 17 | } 18 | return cast.ToString(obj.Start), nil 19 | } 20 | 21 | func (obj *Scope) GetEndString() (string, error) { 22 | if obj.End == nil { 23 | return "", errors.New("end is nil") 24 | } 25 | return cast.ToString(obj.End), nil 26 | } 27 | 28 | func (obj *Scope) GetStartInt64() (int64, error) { 29 | if obj.Start == nil { 30 | return 0, errors.New("start is nil") 31 | } 32 | return cast.ToInt64(obj.Start), nil 33 | } 34 | 35 | func (obj *Scope) GetEndInt64() (int64, error) { 36 | if obj.End == nil { 37 | return 0, errors.New("end is nil") 38 | } 39 | return cast.ToInt64(obj.End), nil 40 | } 41 | 42 | func (obj *Scope) GetStartFloat64() (float64, error) { 43 | if obj.Start == nil { 44 | return 0, errors.New("start is nil") 45 | } 46 | return cast.ToFloat64(obj.Start), nil 47 | } 48 | 49 | func (obj *Scope) GetEndFloat64() (float64, error) { 50 | if obj.End == nil { 51 | return 0, errors.New("end is nil") 52 | } 53 | return cast.ToFloat64(obj.End), nil 54 | } 55 | -------------------------------------------------------------------------------- /toolkit/base/init_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "testing" 4 | 5 | func TestInitPath(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | wantErr bool 9 | }{ 10 | // TODO: Add test cases. 11 | } 12 | for _, tt := range tests { 13 | t.Run(tt.name, func(t *testing.T) { 14 | if err := InitPath(); (err != nil) != tt.wantErr { 15 | t.Errorf("InitPath() error = %v, wantErr %v", err, tt.wantErr) 16 | } 17 | }) 18 | } 19 | } 20 | 21 | func TestInitConfig(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | wantErr bool 25 | }{ 26 | // TODO: Add test cases. 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | if err := InitConfig(); (err != nil) != tt.wantErr { 31 | t.Errorf("InitConfig() error = %v, wantErr %v", err, tt.wantErr) 32 | } 33 | }) 34 | } 35 | } 36 | 37 | func TestInitQuery(t *testing.T) { 38 | tests := []struct { 39 | name string 40 | wantErr bool 41 | }{ 42 | // TODO: Add test cases. 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | if err := InitQuery(); (err != nil) != tt.wantErr { 47 | t.Errorf("InitQuery() error = %v, wantErr %v", err, tt.wantErr) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestInitBase(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | wantErr bool 57 | }{ 58 | // TODO: Add test cases. 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | if err := InitBase(); (err != nil) != tt.wantErr { 63 | t.Errorf("InitBase() error = %v, wantErr %v", err, tt.wantErr) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /toolkit/base/install.go: -------------------------------------------------------------------------------- 1 | //go:build go1.19 2 | // +build go1.19 3 | 4 | package base 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | // GoInstall go get path. 14 | func GoInstall(path ...string) error { 15 | for _, p := range path { 16 | if !strings.Contains(p, "@") { 17 | p += "@latest" 18 | } 19 | fmt.Printf("go install %s\n", p) 20 | cmd := exec.Command("go", "install", p) 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | if err := cmd.Run(); err != nil { 24 | return err 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /toolkit/base/install_compatible.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.19 2 | // +build !go1.19 3 | 4 | package base 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | // GoInstall go get path. 13 | func GoInstall(path ...string) error { 14 | for _, p := range path { 15 | fmt.Printf("go get -u %s\n", p) 16 | cmd := exec.Command("go", "get", "-u", p) 17 | cmd.Stdout = os.Stdout 18 | cmd.Stderr = os.Stderr 19 | if err := cmd.Run(); err != nil { 20 | return err 21 | } 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /toolkit/base/install_compatible_test.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.19 2 | // +build !go1.19 3 | 4 | package base 5 | 6 | import "testing" 7 | 8 | func TestGoInstall(t *testing.T) { 9 | type args struct { 10 | path []string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantErr bool 16 | }{ 17 | // TODO: Add test cases. 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if err := GoInstall(tt.args.path...); (err != nil) != tt.wantErr { 22 | t.Errorf("GoInstall() error = %v, wantErr %v", err, tt.wantErr) 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /toolkit/base/install_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.19 2 | // +build go1.19 3 | 4 | package base 5 | 6 | import "testing" 7 | 8 | func TestGoInstall(t *testing.T) { 9 | type args struct { 10 | path []string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantErr bool 16 | }{ 17 | // TODO: Add test cases. 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if err := GoInstall(tt.args.path...); (err != nil) != tt.wantErr { 22 | t.Errorf("GoInstall() error = %v, wantErr %v", err, tt.wantErr) 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /toolkit/base/mod.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | "golang.org/x/mod/modfile" 12 | ) 13 | 14 | // ModulePath returns go module path. 15 | func ModulePath(filename string) (string, error) { 16 | modBytes, err := os.ReadFile(filename) 17 | if err != nil { 18 | return "", err 19 | } 20 | return modfile.ModulePath(modBytes), nil 21 | } 22 | 23 | // ModuleVersion returns module version. 24 | func ModuleVersion(path string) (string, error) { 25 | stdout := &bytes.Buffer{} 26 | fd := exec.Command("go", "mod", "graph") 27 | fd.Stdout = stdout 28 | fd.Stderr = stdout 29 | if err := fd.Run(); err != nil { 30 | return "", err 31 | } 32 | rd := bufio.NewReader(stdout) 33 | for { 34 | line, _, err := rd.ReadLine() 35 | if err != nil { 36 | return "", err 37 | } 38 | str := string(line) 39 | i := strings.Index(str, "@") 40 | if strings.Contains(str, path+"@") && i != -1 { 41 | return path + str[i:], nil 42 | } 43 | } 44 | } 45 | 46 | // RatelMod returns ratel mod. 47 | func RatelMod() string { 48 | cacheOut, _ := exec.Command("go", "env", "GOMODCACHE").Output() 49 | cachePath := strings.Trim(string(cacheOut), "\n") 50 | pathOut, _ := exec.Command("go", "env", "GOPATH").Output() 51 | gopath := strings.Trim(string(pathOut), "\n") 52 | if cachePath == "" { 53 | cachePath = filepath.Join(gopath, "pkg", "mod") 54 | } 55 | if path, err := ModuleVersion("github.com/abulo/ratel/v3"); err == nil { 56 | return filepath.Join(cachePath, path) 57 | } 58 | return filepath.Join(gopath, "src", "github.com", "abulo", "ratel") 59 | } 60 | -------------------------------------------------------------------------------- /toolkit/base/mod_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "testing" 4 | 5 | func TestModulePath(t *testing.T) { 6 | type args struct { 7 | filename string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | wantErr bool 14 | }{ 15 | // TODO: Add test cases. 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | got, err := ModulePath(tt.args.filename) 20 | if (err != nil) != tt.wantErr { 21 | t.Errorf("ModulePath() error = %v, wantErr %v", err, tt.wantErr) 22 | return 23 | } 24 | if got != tt.want { 25 | t.Errorf("ModulePath() = %v, want %v", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestModuleVersion(t *testing.T) { 32 | type args struct { 33 | path string 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want string 39 | wantErr bool 40 | }{ 41 | // TODO: Add test cases. 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | got, err := ModuleVersion(tt.args.path) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("ModuleVersion() error = %v, wantErr %v", err, tt.wantErr) 48 | return 49 | } 50 | if got != tt.want { 51 | t.Errorf("ModuleVersion() = %v, want %v", got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestRatelMod(t *testing.T) { 58 | tests := []struct { 59 | name string 60 | want string 61 | }{ 62 | // TODO: Add test cases. 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | if got := RatelMod(); got != tt.want { 67 | t.Errorf("RatelMod() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /toolkit/base/vcs_url.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | scpSyntaxRe = regexp.MustCompile(`^(\w+)@([\w.-]+):(.*)$`) 13 | scheme = []string{"git", "https", "http", "git+ssh", "ssh", "file", "ftp", "ftps"} 14 | ) 15 | 16 | // ParseVCSUrl ref https://github.com/golang/go/blob/master/src/cmd/go/internal/vcs/vcs.go 17 | // see https://go-review.googlesource.com/c/go/+/12226/ 18 | // git url define https://git-scm.com/docs/git-clone#_git_urls 19 | func ParseVCSUrl(repo string) (*url.URL, error) { 20 | var ( 21 | repoURL *url.URL 22 | err error 23 | ) 24 | 25 | if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil { 26 | // Match SCP-like syntax and convert it to a URL. 27 | // Eg, "git@github.com:user/repo" becomes 28 | // "ssh://git@github.com/user/repo". 29 | repoURL = &url.URL{ 30 | Scheme: "ssh", 31 | User: url.User(m[1]), 32 | Host: m[2], 33 | Path: m[3], 34 | } 35 | } else { 36 | if !strings.Contains(repo, "//") { 37 | repo = "//" + repo 38 | } 39 | if strings.HasPrefix(repo, "//git@") { 40 | repo = "ssh:" + repo 41 | } else if strings.HasPrefix(repo, "//") { 42 | repo = "https:" + repo 43 | } 44 | repoURL, err = url.Parse(repo) 45 | if err != nil { 46 | return nil, err 47 | } 48 | } 49 | 50 | // Iterate over insecure schemes too, because this function simply 51 | // reports the state of the repo. If we can't see insecure schemes then 52 | // we can't report the actual repo URL. 53 | for _, s := range scheme { 54 | if repoURL.Scheme == s { 55 | return repoURL, nil 56 | } 57 | } 58 | return nil, errors.New("无法解析的仓库地址") 59 | } 60 | -------------------------------------------------------------------------------- /toolkit/base/vcs_url_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParseVCSUrl(t *testing.T) { 10 | type args struct { 11 | repo string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *url.URL 17 | wantErr bool 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | got, err := ParseVCSUrl(tt.args.repo) 24 | if (err != nil) != tt.wantErr { 25 | t.Errorf("ParseVCSUrl() error = %v, wantErr %v", err, tt.wantErr) 26 | return 27 | } 28 | if !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("ParseVCSUrl() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /toolkit/frontend/frontend.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | "github.com/AlecAivazis/survey/v2" 11 | "github.com/abulo/ratel/v3/toolkit/project" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // CmdNew represents the new command. 17 | var CmdNew = &cobra.Command{ 18 | Use: "frontend", 19 | Short: "前端项目", 20 | Long: "前端项目: toolkit frontend helloworld", 21 | Run: run, 22 | } 23 | 24 | var ( 25 | repoURL string 26 | branch string 27 | timeout string 28 | ) 29 | 30 | func init() { 31 | repoURL = "https://github.com/abulo/layout-vue.git" 32 | timeout = "60s" 33 | CmdNew.Flags().StringVarP(&repoURL, "repo-url", "r", repoURL, "layout repo") 34 | CmdNew.Flags().StringVarP(&branch, "branch", "b", branch, "repo branch") 35 | CmdNew.Flags().StringVarP(&timeout, "timeout", "t", timeout, "time out") 36 | } 37 | 38 | func run(cmd *cobra.Command, args []string) { 39 | wd, err := os.Getwd() 40 | if err != nil { 41 | panic(err) 42 | } 43 | t, err := time.ParseDuration(timeout) 44 | if err != nil { 45 | panic(err) 46 | } 47 | ctx, cancel := context.WithTimeout(context.Background(), t) 48 | defer cancel() 49 | name := "" 50 | if len(args) == 0 { 51 | prompt := &survey.Input{ 52 | Message: "项目名称", 53 | Help: "项目命名:字母小写", 54 | } 55 | err = survey.AskOne(prompt, &name) 56 | if err != nil || name == "" { 57 | return 58 | } 59 | } else { 60 | name = args[0] 61 | } 62 | p := &project.Project{Name: path.Base(name), Path: name} 63 | done := make(chan error, 1) 64 | go func() { 65 | done <- p.NewFront(ctx, wd, repoURL, branch) 66 | }() 67 | select { 68 | case <-ctx.Done(): 69 | if errors.Is(ctx.Err(), context.DeadlineExceeded) { 70 | fmt.Fprint(os.Stderr, "\033[31mERROR: 项目创建超时 \033[m\n") 71 | return 72 | } 73 | fmt.Fprintf(os.Stderr, "\033[31mERROR: 项目创建失败(%s)\033[m\n", ctx.Err().Error()) 74 | case err = <-done: 75 | if err != nil { 76 | fmt.Fprintf(os.Stderr, "\033[31mERROR: 项目创建失败(%s)\033[m\n", err.Error()) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /toolkit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/abulo/ratel/v3/core/env" 7 | "github.com/abulo/ratel/v3/toolkit/api" 8 | "github.com/abulo/ratel/v3/toolkit/backend" 9 | "github.com/abulo/ratel/v3/toolkit/build" 10 | "github.com/abulo/ratel/v3/toolkit/change" 11 | "github.com/abulo/ratel/v3/toolkit/dao" 12 | "github.com/abulo/ratel/v3/toolkit/frontend" 13 | "github.com/abulo/ratel/v3/toolkit/module" 14 | "github.com/abulo/ratel/v3/toolkit/upgrade" 15 | "github.com/abulo/ratel/v3/toolkit/vue" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var rootCmd = &cobra.Command{ 20 | Use: "toolkit", 21 | Short: "toolkit: An elegant toolkit for Go microservices.", 22 | Long: `toolkit: An elegant toolkit for Go microservices.`, 23 | Version: env.RatelVersion(), 24 | CompletionOptions: cobra.CompletionOptions{ 25 | DisableDefaultCmd: true, 26 | DisableNoDescFlag: true, 27 | DisableDescriptions: true, 28 | HiddenDefaultCmd: true, 29 | }, 30 | DisableFlagsInUseLine: true, 31 | } 32 | 33 | func init() { 34 | rootCmd.SetHelpCommand(&cobra.Command{ 35 | Use: "no-help", 36 | Hidden: true, 37 | }) 38 | rootCmd.AddCommand(frontend.CmdNew) 39 | rootCmd.AddCommand(backend.CmdNew) 40 | rootCmd.AddCommand(dao.CmdNew) 41 | rootCmd.AddCommand(module.CmdNew) 42 | rootCmd.AddCommand(build.CmdNew) 43 | rootCmd.AddCommand(upgrade.CmdNew) 44 | rootCmd.AddCommand(upgrade.CmdInit) 45 | rootCmd.AddCommand(api.CmdNew) 46 | rootCmd.AddCommand(vue.Vue) 47 | rootCmd.AddCommand(change.CmdNew) 48 | } 49 | 50 | func main() { 51 | if err := rootCmd.Execute(); err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /toolkit/mod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go build -o toolkit main.go; 4 | mv toolkit $GOPATH/bin/; -------------------------------------------------------------------------------- /toolkit/project/add.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | "github.com/AlecAivazis/survey/v2" 10 | "github.com/abulo/ratel/v3/toolkit/base" 11 | "github.com/fatih/color" 12 | ) 13 | 14 | var repoAddIgnores = []string{ 15 | ".git", ".github", ".gitignore", 16 | } 17 | 18 | func (p *Project) Add(ctx context.Context, dir string, layout string, branch string, mod string) error { 19 | to := path.Join(dir, p.Path) 20 | 21 | if _, err := os.Stat(to); !os.IsNotExist(err) { 22 | fmt.Printf("🚫 %s 已经存在\n", p.Name) 23 | override := false 24 | prompt := &survey.Confirm{ 25 | Message: "📂 您想要覆盖文件夹吗 ?", 26 | Help: "删除现有文件夹并创建项目.", 27 | } 28 | e := survey.AskOne(prompt, &override) 29 | if e != nil { 30 | return e 31 | } 32 | if !override { 33 | return err 34 | } 35 | os.RemoveAll(to) 36 | } 37 | 38 | fmt.Printf("🚀 添加服务 %s, 代码仓库是 %s, 请稍候.\n\n", p.Name, layout) 39 | 40 | repo := base.NewRepo(layout, branch) 41 | 42 | if err := repo.CopyToV2(ctx, to, path.Join(mod, p.Path), repoAddIgnores, []string{path.Join(p.Path, "api"), "api"}); err != nil { 43 | return err 44 | } 45 | base.Tree(to, dir) 46 | fmt.Printf("\n🍺 服务添加成功 %s\n", color.GreenString(p.Name)) 47 | fmt.Print("💻 使用以下命令进入项目 👇:\n\n") 48 | fmt.Println(color.WhiteString("$ cd %s", p.Name)) 49 | fmt.Println(" 🤝 感谢使用 Ratel") 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /toolkit/toolkit.toml: -------------------------------------------------------------------------------- 1 | [option] 2 | deleted = true 3 | [db] 4 | Host = "172.18.1.3" # 数据库 IP 5 | Port = "3306" # 数据库端口 6 | Username = "root" # 数据库用户名 7 | Password = "mysql" # 数据库密码 8 | Charset = "utf8mb4" # 数据库字符集 9 | Database = "xiaobao" # 数据库名称 10 | ParseTime = true # 是否解析时间 11 | TimeZone = "PRC" # 数据库时区 12 | SslMode = false # 数据库SSL模式 postgresql 专用 13 | DialTimeOut = "5s" # 连接超时时间 clickhouse 专用 14 | ReadTimeOut = "5s" # 读取超时时间 clickhouse 专用 15 | MaxIdleConns = 64 # 连接池里最大空闲连接数。必须要比maxOpenConns小 16 | MaxOpenConns = 64 # 连接池最大打开连接数 17 | MaxLifetime = 5 # 连接池里面的连接最大存活时长 18 | MaxIdleTime = 5 # 连接池里面的连接最大空闲时长 19 | DisableMetric = false # 关闭指标采集 20 | DisableTrace = false # 关闭链路追踪 21 | DriverName = "mysql" # 数据库驱动名称 22 | [vue] 23 | ModulesDir = "src/api/modules" 24 | InterfaceDir = "src/api/interface" 25 | PageDir = "src/views" 26 | [watch] 27 | # 在 go.mod 发生变化自动运行 go mod tidy 28 | AutoTidy = false 29 | # 为 go build 最后的文件参数,可以为空,表示当前目录。 30 | # MainFiles = "./ratel/tookit" 31 | MainFiles = ["./"] 32 | # 指定可执行文件输出的文件路径 33 | # 为空表示默认值,若不带路径信息,会附加在 Dirs 的第一个路径上; 34 | # windows 系统无须指定 .exe 扩展名,会自行添加。 35 | # 如果带路径信息,则会使用该文件所在目录作为工作目录。 36 | # OutputName = "./ratel/tookit/tookit" 37 | OutputName = ["./tookit"] 38 | # 指定监视的文件扩展名 39 | # 为空表示不监视任何文件,如果指定了 *,表示所有文件类型,包括没有扩展名的文件。 40 | Exts = ["*"] 41 | # 忽略的文件 42 | # 采用 [path.Match] 作为匹配方式。 43 | # glob 格式,高于 Exts 配置项 44 | Excludes = [] 45 | # 传递给编译成功后的程序的参数 46 | # ./server --path=/var/run/aa.config 47 | AppArgs = "" 48 | # 是否监视子目录 49 | Recursive = false 50 | # 表示需要监视的目录 51 | # 至少指定一个目录,第一个目录被当作主目录,将编译其下的文件作为执行主体。 52 | # 如果你在 go.mod 中设置了 replace 或是更高级的 workspace 中有相关设置, 53 | # 可以在此处指定这些需要跟踪的包。 54 | # 如果 OutputName 中未指定目录的话,第一个目录会被当作工作目录使用。 55 | # NOTE: 如果指定的目录下没有需要被监视的文件类型,那么该目录将被忽略。 56 | Dirs=["./"] 57 | # 监视器的更新频率 58 | WatcherFrequency = "5s" 59 | # 传递各个工具的参数 60 | # 大致有以下几个,具体可参考 go build 的 xxflags 系列参数。 61 | # - asm --> asmflags 62 | # - gccgo --> gccgoflags 63 | # - gc --> gcflags 64 | # - ld --> ldflags 65 | Asm = "" 66 | Gccgo = "" 67 | Gc = "" 68 | Ld = "" -------------------------------------------------------------------------------- /toolkit/upgrade/upgrade.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/abulo/ratel/v3/toolkit/base" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // CmdUpgrade represents the upgrade command. 11 | var CmdNew = &cobra.Command{ 12 | Use: "upgrade", 13 | Short: "升级脚手架", 14 | Long: "升级脚手架命令 : toolkit upgrade", 15 | Run: Run, 16 | } 17 | 18 | // CmdUpgrade represents the upgrade command. 19 | var CmdInit = &cobra.Command{ 20 | Use: "init", 21 | Short: "脚手架初始化", 22 | Long: "脚手架初始化 : toolkit init", 23 | Run: Run, 24 | } 25 | 26 | // Run upgrade the ratel tools. 27 | func Run(cmd *cobra.Command, args []string) { 28 | err := base.GoInstall( 29 | "github.com/abulo/ratel/v3/toolkit@latest", 30 | "google.golang.org/protobuf/cmd/protoc-gen-go@latest", 31 | "google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest", 32 | "github.com/google/gnostic/cmd/protoc-gen-openapi@latest", 33 | "github.com/oligot/go-mod-upgrade@latest", 34 | "github.com/syncore/protoc-go-inject-tag@latest", 35 | ) 36 | if err != nil { 37 | fmt.Println(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /util/exec_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestExec(t *testing.T) { 6 | type args struct { 7 | command string 8 | output *[]string 9 | returnVar *int 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want string 15 | }{ 16 | // TODO: Add test cases. 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | if got := Exec(tt.args.command, tt.args.output, tt.args.returnVar); got != tt.want { 21 | t.Errorf("Exec() = %v, want %v", got, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func TestSystem(t *testing.T) { 28 | type args struct { 29 | command string 30 | returnVar *int 31 | } 32 | tests := []struct { 33 | name string 34 | args args 35 | want string 36 | }{ 37 | // TODO: Add test cases. 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := System(tt.args.command, tt.args.returnVar); got != tt.want { 42 | t.Errorf("System() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestPassThru(t *testing.T) { 49 | type args struct { 50 | command string 51 | returnVar *int 52 | } 53 | tests := []struct { 54 | name string 55 | args args 56 | }{ 57 | // TODO: Add test cases. 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | PassThru(tt.args.command, tt.args.returnVar) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /util/grpc.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "time" 5 | 6 | "google.golang.org/protobuf/types/known/timestamppb" 7 | ) 8 | 9 | func GrpcTime(x *timestamppb.Timestamp) time.Time { 10 | return time.Unix(int64(x.GetSeconds()), int64(x.GetNanos())).Local() 11 | } 12 | -------------------------------------------------------------------------------- /util/init.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func Do(attempts int, sleep time.Duration, f func() error) error { 9 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 10 | if err := f(); err != nil { 11 | if attempts--; attempts >= 0 { 12 | // Add some randomness to prevent creating a Thundering Herd 13 | jitter := time.Duration(r.Int63n(int64(sleep))) 14 | sleep = sleep + jitter/2 15 | time.Sleep(sleep) 16 | return Do(attempts, 2*sleep, f) 17 | } 18 | return err 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /util/init_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDo(t *testing.T) { 9 | type args struct { 10 | attempts int 11 | sleep time.Duration 12 | f func() error 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | wantErr bool 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if err := Do(tt.args.attempts, tt.args.sleep, tt.args.f); (err != nil) != tt.wantErr { 24 | t.Errorf("Do() error = %v, wantErr %v", err, tt.wantErr) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /util/mongodb.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | ) 11 | 12 | // Pipeline gets aggregation pipeline from a string 13 | func Pipeline(str string) mongo.Pipeline { 14 | var pipeline = []bson.D{} 15 | str = strings.TrimSpace(str) 16 | if strings.Index(str, "[") != 0 { 17 | var doc bson.M 18 | json.Unmarshal([]byte(str), &doc) 19 | var v bson.D 20 | b, _ := bson.Marshal(doc) 21 | bson.Unmarshal(b, &v) 22 | pipeline = append(pipeline, v) 23 | } else { 24 | var docs []bson.M 25 | json.Unmarshal([]byte(str), &docs) 26 | for _, doc := range docs { 27 | var v bson.D 28 | b, _ := bson.Marshal(doc) 29 | bson.Unmarshal(b, &v) 30 | pipeline = append(pipeline, v) 31 | } 32 | } 33 | return pipeline 34 | } 35 | 36 | // ConvertBson 构建 bson.D 查询数据 37 | func ConvertBson(items bson.D, item bson.E) bson.D { 38 | key := make([]string, 0) 39 | for _, v := range items { 40 | key = append(key, v.Key) 41 | } 42 | if !InArray(item.Key, key) { 43 | items = append(items, item) 44 | return items 45 | } 46 | //再次赋值 47 | for i, newItem := range items { 48 | if newItem.Key == item.Key { 49 | tp := indirect(newItem.Value) 50 | switch tp.(type) { 51 | case bson.D: 52 | tran := items[i].Value.(bson.D) 53 | appendVal := item.Value.(bson.D) 54 | tran = append(tran, appendVal...) 55 | items[i].Value = tran 56 | default: 57 | items[i].Value = item.Value 58 | } 59 | } 60 | } 61 | return items 62 | } 63 | 64 | // From html/template/content.go 65 | // Copyright 2011 The Go Authors. All rights reserved. 66 | // indirect returns the value, after dereferencing as many times 67 | // as necessary to reach the base type (or nil). 68 | func indirect(a any) any { 69 | if a == nil { 70 | return nil 71 | } 72 | if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { 73 | // Avoid creating a reflect.Value if it's not a pointer. 74 | return a 75 | } 76 | v := reflect.ValueOf(a) 77 | for v.Kind() == reflect.Ptr && !v.IsNil() { 78 | v = v.Elem() 79 | } 80 | return v.Interface() 81 | } 82 | -------------------------------------------------------------------------------- /util/mongodb_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "go.mongodb.org/mongo-driver/bson" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | ) 10 | 11 | func TestPipeline(t *testing.T) { 12 | type args struct { 13 | str string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want mongo.Pipeline 19 | }{ 20 | // TODO: Add test cases. 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | if got := Pipeline(tt.args.str); !reflect.DeepEqual(got, tt.want) { 25 | t.Errorf("Pipeline() = %v, want %v", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestConvertBson(t *testing.T) { 32 | type args struct { 33 | items bson.D 34 | item bson.E 35 | } 36 | tests := []struct { 37 | name string 38 | args args 39 | want bson.D 40 | }{ 41 | // TODO: Add test cases. 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | if got := ConvertBson(tt.args.items, tt.args.item); !reflect.DeepEqual(got, tt.want) { 46 | t.Errorf("ConvertBson() = %v, want %v", got, tt.want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func Test_indirect(t *testing.T) { 53 | type args struct { 54 | a any 55 | } 56 | tests := []struct { 57 | name string 58 | args args 59 | want any 60 | }{ 61 | // TODO: Add test cases. 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | if got := indirect(tt.args.a); !reflect.DeepEqual(got, tt.want) { 66 | t.Errorf("indirect() = %v, want %v", got, tt.want) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /util/pinyin.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "unicode" 5 | 6 | "github.com/mozillazg/go-pinyin" 7 | ) 8 | 9 | // ZhCharToFirstPinyin 将中文转换成小写字母 10 | func ZhCharToFirstPinyin(p string) string { 11 | var a = pinyin.NewArgs() 12 | var s string 13 | a.Style = pinyin.FirstLetter 14 | for _, r := range p { 15 | if unicode.Is(unicode.Han, r) { 16 | s += string(pinyin.Pinyin(string(r), a)[0][0]) 17 | } else if unicode.IsNumber(r) || unicode.IsLetter(r) { 18 | s += string(r) 19 | } 20 | } 21 | return StrToLower(s) 22 | } 23 | 24 | // ZhCharToPinyin 将中文转换成小写字母 25 | func ZhCharToPinyin(p string) string { 26 | var a = pinyin.NewArgs() 27 | var s string 28 | a.Style = pinyin.Normal 29 | for _, r := range p { 30 | if unicode.Is(unicode.Han, r) { 31 | s += string(pinyin.Pinyin(string(r), a)[0][0]) 32 | } else if unicode.IsNumber(r) || unicode.IsLetter(r) { 33 | s += string(r) 34 | } 35 | } 36 | return StrToLower(s) 37 | } 38 | -------------------------------------------------------------------------------- /util/pinyin_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestZhCharToFirstPinyin(t *testing.T) { 6 | type args struct { 7 | p string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := ZhCharToFirstPinyin(tt.args.p); got != tt.want { 19 | t.Errorf("ZhCharToFirstPinyin() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func TestZhCharToPinyin(t *testing.T) { 26 | type args struct { 27 | p string 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want string 33 | }{ 34 | // TODO: Add test cases. 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := ZhCharToPinyin(tt.args.p); got != tt.want { 39 | t.Errorf("ZhCharToPinyin() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /util/scprng.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | // RandomBytes random_bytes() 9 | func RandomBytes(length int) ([]byte, error) { 10 | bs := make([]byte, length) 11 | _, err := rand.Read(bs) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return bs, nil 17 | } 18 | 19 | // RandomInt random_int() 20 | func RandomInt(min, max int) (int, error) { 21 | if min > max { 22 | panic("argument #1 must be less than or equal to argument #2") 23 | } 24 | 25 | if min == max { 26 | return min, nil 27 | } 28 | nb, err := rand.Int(rand.Reader, big.NewInt(int64(max+1-min))) 29 | if err != nil { 30 | return 0, err 31 | } 32 | return int(nb.Int64()) + min, nil 33 | } 34 | 35 | // StrPad 填充字符串 36 | func StrPad(str1, str2 string, i int) string { 37 | n := i - len(str1) - len(str2) 38 | if n > 0 { 39 | for i := 0; i < n; i++ { 40 | str1 = str1 + "0" 41 | } 42 | } 43 | return str1 + str2 44 | } 45 | -------------------------------------------------------------------------------- /util/scprng_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRandomBytes(t *testing.T) { 9 | type args struct { 10 | length int 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want []byte 16 | wantErr bool 17 | }{ 18 | // TODO: Add test cases. 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | got, err := RandomBytes(tt.args.length) 23 | if (err != nil) != tt.wantErr { 24 | t.Errorf("RandomBytes() error = %v, wantErr %v", err, tt.wantErr) 25 | return 26 | } 27 | if !reflect.DeepEqual(got, tt.want) { 28 | t.Errorf("RandomBytes() = %v, want %v", got, tt.want) 29 | } 30 | }) 31 | } 32 | } 33 | 34 | func TestRandomInt(t *testing.T) { 35 | type args struct { 36 | min int 37 | max int 38 | } 39 | tests := []struct { 40 | name string 41 | args args 42 | want int 43 | wantErr bool 44 | }{ 45 | // TODO: Add test cases. 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | got, err := RandomInt(tt.args.min, tt.args.max) 50 | if (err != nil) != tt.wantErr { 51 | t.Errorf("RandomInt() error = %v, wantErr %v", err, tt.wantErr) 52 | return 53 | } 54 | if got != tt.want { 55 | t.Errorf("RandomInt() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestStrPad(t *testing.T) { 62 | type args struct { 63 | str1 string 64 | str2 string 65 | i int 66 | } 67 | tests := []struct { 68 | name string 69 | args args 70 | want string 71 | }{ 72 | // TODO: Add test cases. 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | if got := StrPad(tt.args.str1, tt.args.str2, tt.args.i); got != tt.want { 77 | t.Errorf("StrPad() = %v, want %v", got, tt.want) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /util/var_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestEmpty(t *testing.T) { 6 | type args struct { 7 | val any 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want bool 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := Empty(tt.args.val); got != tt.want { 19 | t.Errorf("Empty() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | 25 | func TestIsNumeric(t *testing.T) { 26 | type args struct { 27 | val any 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want bool 33 | }{ 34 | // TODO: Add test cases. 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := IsNumeric(tt.args.val); got != tt.want { 39 | t.Errorf("IsNumeric() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /watch/logger.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/issue9/term/v3/colors" 8 | ) 9 | 10 | // 日志类型 11 | const ( 12 | LogTypeSuccess int8 = iota 13 | LogTypeInfo 14 | LogTypeWarn 15 | LogTypeError 16 | LogTypeIgnore // 默认情况下被忽略的信息,一般内容比较多,且价格不高的内容会显示在此通道。 17 | LogTypeApp // 被编译程序返回的信息 18 | LogTypeGo // Go 编译器返回的信息 19 | ) 20 | 21 | type ( 22 | // Logger 热编译过程中的日志接收对象 23 | Logger interface { 24 | // Output 输出日志内容 25 | // 26 | // t 表示日志类型,一般表示日志的重要程度或是日志的来源信息。 27 | Output(t int8, message string) 28 | } 29 | 30 | loggerWriter struct { 31 | t int8 32 | w Logger 33 | } 34 | 35 | consoleLogger struct { 36 | showIgnore bool 37 | writers map[int8]*consoleWriter 38 | } 39 | 40 | consoleWriter struct { 41 | out io.Writer 42 | color colors.Color 43 | prefix string 44 | } 45 | ) 46 | 47 | func (w *loggerWriter) Write(bs []byte) (int, error) { 48 | w.w.Output(w.t, string(bs)) 49 | return len(bs), nil 50 | } 51 | 52 | func asWriter(t int8, w Logger) io.Writer { return &loggerWriter{t: t, w: w} } 53 | 54 | func (c *consoleLogger) Output(t int8, msg string) { 55 | if !c.showIgnore && t == LogTypeIgnore { 56 | return 57 | } 58 | 59 | w := c.writers[t] 60 | colors.Fprint(w.out, colors.Normal, w.color, colors.Default, w.prefix) 61 | msg = strings.TrimRight(msg, "\n") 62 | colors.Fprintln(w.out, colors.Normal, colors.Default, colors.Default, msg) 63 | } 64 | 65 | // NewConsoleLogger 返回将日志输出到控制台的 Logger 接口实现 66 | func NewConsoleLogger(showIgnore bool, err, out io.Writer) Logger { 67 | newCW := func(out io.Writer, color colors.Color, prefix string) *consoleWriter { 68 | return &consoleWriter{ 69 | out: out, 70 | color: color, 71 | prefix: prefix, 72 | } 73 | } 74 | 75 | return &consoleLogger{ 76 | showIgnore: showIgnore, 77 | writers: map[int8]*consoleWriter{ 78 | LogTypeSuccess: newCW(out, colors.Green, "[SUCC] "), 79 | LogTypeInfo: newCW(out, colors.Blue, "[INFO] "), 80 | LogTypeWarn: newCW(err, colors.Magenta, "[WARN] "), 81 | LogTypeError: newCW(err, colors.Red, "[ERRO] "), 82 | LogTypeIgnore: newCW(out, colors.Default, "[IGNO] "), 83 | LogTypeApp: newCW(out, colors.Default, "[APP] "), 84 | LogTypeGo: newCW(out, colors.Default, "[GO] "), 85 | }, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /watch/logger_test.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func Test_loggerWriter_Write(t *testing.T) { 10 | type args struct { 11 | bs []byte 12 | } 13 | tests := []struct { 14 | name string 15 | w *loggerWriter 16 | args args 17 | want int 18 | wantErr bool 19 | }{ 20 | // TODO: Add test cases. 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | got, err := tt.w.Write(tt.args.bs) 25 | if (err != nil) != tt.wantErr { 26 | t.Errorf("loggerWriter.Write() error = %v, wantErr %v", err, tt.wantErr) 27 | return 28 | } 29 | if got != tt.want { 30 | t.Errorf("loggerWriter.Write() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func Test_consoleLogger_Output(t *testing.T) { 37 | type args struct { 38 | t int8 39 | msg string 40 | } 41 | tests := []struct { 42 | name string 43 | c *consoleLogger 44 | args args 45 | }{ 46 | // TODO: Add test cases. 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | tt.c.Output(tt.args.t, tt.args.msg) 51 | }) 52 | } 53 | } 54 | 55 | func TestNewConsoleLogger(t *testing.T) { 56 | type args struct { 57 | showIgnore bool 58 | } 59 | tests := []struct { 60 | name string 61 | args args 62 | want Logger 63 | wantErr string 64 | wantOut string 65 | }{ 66 | // TODO: Add test cases. 67 | } 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | err := &bytes.Buffer{} 71 | out := &bytes.Buffer{} 72 | if got := NewConsoleLogger(tt.args.showIgnore, err, out); !reflect.DeepEqual(got, tt.want) { 73 | t.Errorf("NewConsoleLogger() = %v, want %v", got, tt.want) 74 | } 75 | if gotErr := err.String(); gotErr != tt.wantErr { 76 | t.Errorf("NewConsoleLogger() = %v, want %v", gotErr, tt.wantErr) 77 | } 78 | if gotOut := out.String(); gotOut != tt.wantOut { 79 | t.Errorf("NewConsoleLogger() = %v, want %v", gotOut, tt.wantOut) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /worker/job/job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | // Runner ... 4 | type Runner interface { 5 | Run() 6 | } 7 | -------------------------------------------------------------------------------- /worker/worker.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | // Worker could scheduled by ratel or customized scheduler 4 | type Worker interface { 5 | WorkerStart() error 6 | WorkerStop() error 7 | } 8 | --------------------------------------------------------------------------------